From 222fa48ae49b7032e6cae3e3bcb5bb2630f16174 Mon Sep 17 00:00:00 2001 From: Justin D'Errico Date: Thu, 8 Sep 2022 12:07:01 -0400 Subject: [PATCH] feat(compound): Migrate to template (#1370) --- ...mpound.borrow.contract-position-fetcher.ts | 153 ++++++++++++++++++ ...und.claimable.contract-position-fetcher.ts | 82 ++++++++++ .../common/compound.position-presenter.ts | 55 +++++++ .../common/compound.supply.token-fetcher.ts | 110 +++++++++++++ src/apps/compound/compound.module.ts | 4 +- .../ethereum/compound.balance-fetcher.ts | 77 --------- ...mpound.borrow.contract-position-fetcher.ts | 78 ++++++--- ...und.claimable.contract-position-fetcher.ts | 37 ++--- .../ethereum/compound.position-presenter.ts | 12 ++ .../ethereum/compound.supply.token-fetcher.ts | 73 +++++---- .../pika-protocol-v3.balance-fetcher.ts | 4 +- 11 files changed, 527 insertions(+), 158 deletions(-) create mode 100644 src/apps/compound/common/compound.borrow.contract-position-fetcher.ts create mode 100644 src/apps/compound/common/compound.claimable.contract-position-fetcher.ts create mode 100644 src/apps/compound/common/compound.position-presenter.ts create mode 100644 src/apps/compound/common/compound.supply.token-fetcher.ts delete mode 100644 src/apps/compound/ethereum/compound.balance-fetcher.ts create mode 100644 src/apps/compound/ethereum/compound.position-presenter.ts diff --git a/src/apps/compound/common/compound.borrow.contract-position-fetcher.ts b/src/apps/compound/common/compound.borrow.contract-position-fetcher.ts new file mode 100644 index 000000000..c46c07471 --- /dev/null +++ b/src/apps/compound/common/compound.borrow.contract-position-fetcher.ts @@ -0,0 +1,153 @@ +import { BigNumberish, Contract } from 'ethers'; + +import { ETH_ADDR_ALIAS, ZERO_ADDRESS } from '~app-toolkit/constants/address'; +import { BLOCKS_PER_DAY } from '~app-toolkit/constants/blocks'; +import { buildDollarDisplayItem } from '~app-toolkit/helpers/presentation/display-item.present'; +import { getLabelFromToken } from '~app-toolkit/helpers/presentation/image.present'; +import { isMulticallUnderlyingError } from '~multicall/multicall.ethers'; +import { DisplayProps } from '~position/display.interface'; +import { MetaType } from '~position/position.interface'; +import { ContractPositionTemplatePositionFetcher } from '~position/template/contract-position.template.position-fetcher'; +import { + DefaultContractPositionDefinition, + GetDataPropsParams, + GetDefinitionsParams, + GetDisplayPropsParams, + GetTokenBalancesParams, + GetTokenDefinitionsParams, + UnderlyingTokenDefinition, +} from '~position/template/contract-position.template.types'; + +export type CompoundBorrowTokenDataProps = { + apy: number; + liquidity: number; + isActive: boolean; +}; + +export abstract class CompoundBorrowContractPositionFetcher< + R extends Contract, + S extends Contract, +> extends ContractPositionTemplatePositionFetcher { + abstract comptrollerAddress: string; + abstract getCompoundCTokenContract(address: string): R; + abstract getCompoundComptrollerContract(address: string): S; + + abstract getMarkets(contract: S): Promise; + abstract getUnderlyingAddress(contract: R): Promise; + abstract getExchangeRate(contract: R): Promise; + abstract getBorrowRate(contract: R): Promise; + abstract getCash(contract: R): Promise; + abstract getBorrowBalance(opts: { address: string; contract: R }): Promise; + abstract getCTokenSupply(contract: R): Promise; + abstract getCTokenDecimals(contract: R): Promise; + + getContract(address: string): R { + return this.getCompoundCTokenContract(address); + } + + async getDefinitions({ multicall }: GetDefinitionsParams): Promise { + const comptroller = this.getCompoundComptrollerContract(this.comptrollerAddress); + const addresses = await this.getMarkets(multicall.wrap(comptroller)); + return addresses.map(addr => ({ address: addr.toLowerCase() })); + } + + async getTokenDefinitions({ + contract, + }: GetTokenDefinitionsParams): Promise { + const underlyingAddressRaw = await this.getUnderlyingAddress(contract).catch(err => { + // if the underlying call failed, it's the compound-wrapped native token + if (isMulticallUnderlyingError(err)) return ZERO_ADDRESS; + throw err; + }); + + const underlyingAddress = underlyingAddressRaw.toLowerCase().replace(ETH_ADDR_ALIAS, ZERO_ADDRESS); + return [{ address: underlyingAddress, metaType: MetaType.BORROWED }]; + } + + async getLabel({ + contractPosition, + }: GetDisplayPropsParams): Promise { + const [underlyingToken] = contractPosition.tokens; + return getLabelFromToken(underlyingToken); + } + + async getSecondaryLabel({ + contractPosition, + }: GetDisplayPropsParams): Promise { + const [underlyingToken] = contractPosition.tokens; + return buildDollarDisplayItem(underlyingToken.price); + } + + protected getDenormalizedRate({ + blocksPerDay, + rate, + }: { + rate: BigNumberish; + blocksPerDay: number; + decimals: number; + }): number { + return 100 * (Math.pow(1 + (blocksPerDay * Number(rate)) / Number(1e18), 365) - 1); + } + + protected async getApy({ contract, contractPosition }: GetDataPropsParams) { + const [underlyingToken] = contractPosition.tokens; + const borrowRate = await this.getBorrowRate(contract); + const blocksPerDay = BLOCKS_PER_DAY[this.network]; + return this.getDenormalizedRate({ + blocksPerDay, + rate: borrowRate, + decimals: underlyingToken.decimals, + }); + } + + protected async getPricePerShare({ + contract, + contractPosition, + }: GetDataPropsParams) { + const [underlyingToken] = contractPosition.tokens; + const rateRaw = await this.getExchangeRate(contract); + const mantissa = underlyingToken.decimals + 10; + + return Number(rateRaw) / 10 ** mantissa; + } + + async getDataProps(opts: GetDataPropsParams): Promise { + const { contractPosition, contract } = opts; + const [underlyingToken] = contractPosition.tokens; + + const [decimals, supplyRaw, pricePerShare, apy, cashRaw] = await Promise.all([ + this.getCTokenDecimals(contract), + this.getCTokenSupply(contract), + this.getPricePerShare(opts), + this.getApy(opts), + this.getCash(contract).catch(e => { + if (isMulticallUnderlyingError(e)) return 0; + throw e; + }), + ]); + + const supply = Number(supplyRaw) / 10 ** decimals; + const price = pricePerShare * underlyingToken.price; + const underlyingLiquidity = price * supply; + + // The "cash" needs to be converted back into a proper number format. + // We use the underlying token as the basis for the conversion. + const cashSupply = Number(cashRaw) / 10 ** underlyingToken.decimals; + // Liquidity is the total supply of "cash" multiplied by the price of an underlying token + const borrowedPositionliquidity = cashSupply * underlyingToken.price; + + const borrowLiquidity = + borrowedPositionliquidity > underlyingLiquidity ? 0 : underlyingLiquidity - borrowedPositionliquidity; + + return { + liquidity: -borrowLiquidity, + isActive: Boolean(borrowLiquidity > 0), + apy, + }; + } + + async getTokenBalancesPerPosition({ address, contract }: GetTokenBalancesParams) { + const balanceRaw = await this.getBorrowBalance({ contract, address }); + return [balanceRaw]; + } +} diff --git a/src/apps/compound/common/compound.claimable.contract-position-fetcher.ts b/src/apps/compound/common/compound.claimable.contract-position-fetcher.ts new file mode 100644 index 000000000..b63f7ab8a --- /dev/null +++ b/src/apps/compound/common/compound.claimable.contract-position-fetcher.ts @@ -0,0 +1,82 @@ +import { Inject } from '@nestjs/common'; +import { BigNumberish } from 'ethers'; + +import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface'; +import { getLabelFromToken } from '~app-toolkit/helpers/presentation/image.present'; +import { MetaType } from '~position/position.interface'; +import { ContractPositionTemplatePositionFetcher } from '~position/template/contract-position.template.position-fetcher'; +import { + DefaultContractPositionDefinition, + GetDataPropsParams, + GetDisplayPropsParams, + GetTokenBalancesParams, + GetTokenDefinitionsParams, + UnderlyingTokenDefinition, +} from '~position/template/contract-position.template.types'; + +import { CompoundComptroller, CompoundContractFactory } from '../contracts'; + +export type CompoundClaimablePositionDataProps = { + lensAddress: string; +}; + +export abstract class CompoundClaimableContractPositionFetcher extends ContractPositionTemplatePositionFetcher { + abstract lensAddress: string; + abstract rewardTokenAddress: string; + abstract comptrollerAddress: string; + + constructor( + @Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit, + @Inject(CompoundContractFactory) private readonly contractFactory: CompoundContractFactory, + ) { + super(appToolkit); + } + + getContract(address: string): CompoundComptroller { + return this.contractFactory.compoundComptroller({ address, network: this.network }); + } + + async getDefinitions(): Promise { + return [{ address: this.comptrollerAddress }]; + } + + async getTokenDefinitions( + _opts: GetTokenDefinitionsParams, + ): Promise { + return [{ address: this.rewardTokenAddress, metaType: MetaType.CLAIMABLE }]; + } + + async getLabel({ contractPosition }: GetDisplayPropsParams) { + const rewardToken = contractPosition.tokens[0]; + return `Claimable ${getLabelFromToken(rewardToken)}`; + } + + async getDataProps( + _params: GetDataPropsParams, + ): Promise { + return { + lensAddress: this.lensAddress, + }; + } + + async getTokenBalancesPerPosition({ + address, + contractPosition, + }: GetTokenBalancesParams): Promise { + const [rewardToken] = contractPosition.tokens; + const { + address: comptrollerAddress, + dataProps: { lensAddress }, + } = contractPosition; + + const lensContract = this.contractFactory.compoundLens({ address: lensAddress, network: this.network }); + const rewardMetadata = await lensContract.callStatic.getCompBalanceMetadataExt( + rewardToken.address, + comptrollerAddress, + address, + ); + + const rewardBalanceRaw = rewardMetadata[3]; + return [rewardBalanceRaw]; + } +} diff --git a/src/apps/compound/common/compound.position-presenter.ts b/src/apps/compound/common/compound.position-presenter.ts new file mode 100644 index 000000000..4308339c4 --- /dev/null +++ b/src/apps/compound/common/compound.position-presenter.ts @@ -0,0 +1,55 @@ +import { sumBy } from 'lodash'; + +import { Register } from '~app-toolkit/decorators'; +import { PresentationConfig } from '~app/app.interface'; +import { PositionPresenterTemplate, ReadonlyBalances } from '~position/template/position-presenter.template'; + +export abstract class CompoundPositionPresenter extends PositionPresenterTemplate { + explorePresentationConfig?: PresentationConfig = { + tabs: [ + { + label: 'Lending', + viewType: 'split', + views: [ + { + viewType: 'list', + label: 'Supply', + groupIds: ['supply'], + }, + { + viewType: 'list', + label: 'Borrow', + groupIds: ['borrow'], + }, + ], + }, + ], + }; + + @Register.BalanceProductMeta('Lending') + async getLendingMeta(_address: string, balances: ReadonlyBalances) { + const collaterals = balances.filter(balance => balance.balanceUSD > 0); + const debt = balances.filter(balance => balance.balanceUSD < 0); + const totalCollateralUSD = sumBy(collaterals, a => a.balanceUSD); + const totalDebtUSD = sumBy(debt, a => a.balanceUSD); + const utilRatio = (Math.abs(totalDebtUSD) / totalCollateralUSD) * 100; + + return [ + { + label: 'Collateral', + value: totalCollateralUSD, + type: 'dollar', + }, + { + label: 'Debt', + value: totalDebtUSD, + type: 'dollar', + }, + { + label: 'Utilization Rate', + value: utilRatio, + type: 'pct', + }, + ]; + } +} diff --git a/src/apps/compound/common/compound.supply.token-fetcher.ts b/src/apps/compound/common/compound.supply.token-fetcher.ts new file mode 100644 index 000000000..f93d9e612 --- /dev/null +++ b/src/apps/compound/common/compound.supply.token-fetcher.ts @@ -0,0 +1,110 @@ +import { BigNumberish, Contract } from 'ethers'; + +import { ETH_ADDR_ALIAS, ZERO_ADDRESS } from '~app-toolkit/constants/address'; +import { BLOCKS_PER_DAY } from '~app-toolkit/constants/blocks'; +import { isMulticallUnderlyingError } from '~multicall/multicall.ethers'; +import { BalanceDisplayMode, DisplayProps } from '~position/display.interface'; +import { ExchangeableAppTokenDataProps } from '~position/position.interface'; +import { AppTokenTemplatePositionFetcher } from '~position/template/app-token.template.position-fetcher'; +import { + GetAddressesParams, + GetDataPropsParams, + GetDisplayPropsParams, + GetPricePerShareParams, + GetUnderlyingTokensParams, +} from '~position/template/app-token.template.types'; + +export type CompoundSupplyTokenDataProps = ExchangeableAppTokenDataProps & { + apy: number; + liquidity: number; +}; + +export abstract class CompoundSupplyTokenFetcher< + R extends Contract, + S extends Contract, +> extends AppTokenTemplatePositionFetcher { + protected isExchangeable = false; + + abstract comptrollerAddress: string; + + abstract getCompoundCTokenContract(address: string): R; + abstract getCompoundComptrollerContract(address: string): S; + + abstract getMarkets(contract: S): Promise; + abstract getUnderlyingAddress(contract: R): Promise; + abstract getExchangeRate(contract: R): Promise; + abstract getSupplyRate(contract: R): Promise; + + getContract(address: string): R { + return this.getCompoundCTokenContract(address); + } + + async getAddresses({ multicall }: GetAddressesParams) { + const comptroller = this.getCompoundComptrollerContract(this.comptrollerAddress); + return this.getMarkets(multicall.wrap(comptroller)); + } + + async getUnderlyingTokenAddresses({ contract }: GetUnderlyingTokensParams) { + const underlyingAddressRaw = await this.getUnderlyingAddress(contract).catch(err => { + // if the underlying call failed, it's the compound-wrapped native token + if (isMulticallUnderlyingError(err)) return ZERO_ADDRESS; + throw err; + }); + + return underlyingAddressRaw.toLowerCase().replace(ETH_ADDR_ALIAS, ZERO_ADDRESS); + } + + protected getDenormalizedRate({ + blocksPerDay, + rate, + }: { + rate: BigNumberish; + blocksPerDay: number; + decimals: number; + }): number { + return 100 * (Math.pow(1 + (blocksPerDay * Number(rate)) / Number(1e18), 365) - 1); + } + + async getPricePerShare({ contract, appToken }: GetPricePerShareParams) { + const [underlyingToken] = appToken.tokens; + const rateRaw = await this.getExchangeRate(contract); + const mantissa = underlyingToken.decimals + 10; + + return Number(rateRaw) / 10 ** mantissa; + } + + async getLabel({ appToken }: GetDisplayPropsParams): Promise { + const [underlyingToken] = appToken.tokens; + return underlyingToken.symbol; + } + + async getLabelDetailed({ + appToken, + }: GetDisplayPropsParams): Promise { + return appToken.symbol; + } + + async getBalanceDisplayMode( + _params: GetDisplayPropsParams, + ): Promise { + return BalanceDisplayMode.UNDERLYING; + } + + protected async getApy({ contract, appToken }: GetDataPropsParams) { + const [underlyingToken] = appToken.tokens; + const supplyRate = await this.getSupplyRate(contract); + const blocksPerDay = BLOCKS_PER_DAY[this.network]; + return this.getDenormalizedRate({ + blocksPerDay, + rate: supplyRate, + decimals: underlyingToken.decimals, + }); + } + + async getDataProps(opts: GetDataPropsParams): Promise { + const { appToken } = opts; + const apy = await this.getApy(opts); + const liquidity = appToken.price * appToken.supply; + return { apy, liquidity, exchangeable: this.isExchangeable }; + } +} diff --git a/src/apps/compound/compound.module.ts b/src/apps/compound/compound.module.ts index 1f3ecd526..85ac8053b 100644 --- a/src/apps/compound/compound.module.ts +++ b/src/apps/compound/compound.module.ts @@ -3,9 +3,9 @@ import { AbstractApp } from '~app/app.dynamic-module'; import { CompoundAppDefinition, COMPOUND_DEFINITION } from './compound.definition'; import { CompoundContractFactory } from './contracts'; -import { EthereumCompoundBalanceFetcher } from './ethereum/compound.balance-fetcher'; import { EthereumCompoundBorrowContractPositionFetcher } from './ethereum/compound.borrow.contract-position-fetcher'; import { EthereumCompoundClaimableContractPositionFetcher } from './ethereum/compound.claimable.contract-position-fetcher'; +import { EthereumCompoundPositionPresenter } from './ethereum/compound.position-presenter'; import { EthereumCompoundSupplyTokenFetcher } from './ethereum/compound.supply.token-fetcher'; import { CompoundBorrowBalanceHelper } from './helper/compound.borrow.balance-helper'; import { CompoundBorrowContractPositionHelper } from './helper/compound.borrow.contract-position-helper'; @@ -20,8 +20,8 @@ import { CompoundSupplyTokenHelper } from './helper/compound.supply.token-helper providers: [ CompoundAppDefinition, CompoundContractFactory, - EthereumCompoundBalanceFetcher, EthereumCompoundBorrowContractPositionFetcher, + EthereumCompoundPositionPresenter, EthereumCompoundClaimableContractPositionFetcher, EthereumCompoundSupplyTokenFetcher, // Helpers diff --git a/src/apps/compound/ethereum/compound.balance-fetcher.ts b/src/apps/compound/ethereum/compound.balance-fetcher.ts deleted file mode 100644 index 7db285c6d..000000000 --- a/src/apps/compound/ethereum/compound.balance-fetcher.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Inject } from '@nestjs/common'; - -import { Register } from '~app-toolkit/decorators'; -import { presentBalanceFetcherResponse } from '~app-toolkit/helpers/presentation/balance-fetcher-response.present'; -import { BalanceFetcher } from '~balance/balance-fetcher.interface'; -import { Network } from '~types/network.interface'; - -import { COMPOUND_DEFINITION } from '../compound.definition'; -import { CompoundContractFactory } from '../contracts'; -import { CompoundBorrowBalanceHelper } from '../helper/compound.borrow.balance-helper'; -import { CompoundClaimableBalanceHelper } from '../helper/compound.claimable.balance-helper'; -import { CompoundLendingMetaHelper } from '../helper/compound.lending.meta-helper'; -import { CompoundSupplyBalanceHelper } from '../helper/compound.supply.balance-helper'; - -const appId = COMPOUND_DEFINITION.id; -const network = Network.ETHEREUM_MAINNET; - -@Register.BalanceFetcher(appId, network) -export class EthereumCompoundBalanceFetcher implements BalanceFetcher { - constructor( - @Inject(CompoundBorrowBalanceHelper) - private readonly compoundBorrowBalanceHelper: CompoundBorrowBalanceHelper, - @Inject(CompoundSupplyBalanceHelper) - private readonly compoundSupplyBalanceHelper: CompoundSupplyBalanceHelper, - @Inject(CompoundClaimableBalanceHelper) - private readonly compoundClaimableBalanceHelper: CompoundClaimableBalanceHelper, - @Inject(CompoundLendingMetaHelper) - private readonly compoundLendingMetaHelper: CompoundLendingMetaHelper, - @Inject(CompoundContractFactory) - private readonly compoundContractFactory: CompoundContractFactory, - ) {} - - async getSupplyBalances(address: string) { - return this.compoundSupplyBalanceHelper.getBalances({ - address, - appId, - groupId: COMPOUND_DEFINITION.groups.supply.id, - network, - getTokenContract: ({ address, network }) => this.compoundContractFactory.compoundCToken({ address, network }), - getBalanceRaw: ({ contract, address, multicall }) => multicall.wrap(contract).balanceOf(address), - }); - } - - async getBorrowBalances(address: string) { - return this.compoundBorrowBalanceHelper.getBalances({ - address, - appId, - groupId: COMPOUND_DEFINITION.groups.borrow.id, - network, - getTokenContract: ({ address, network }) => this.compoundContractFactory.compoundCToken({ address, network }), - getBorrowBalanceRaw: ({ contract, address, multicall }) => multicall.wrap(contract).borrowBalanceCurrent(address), - }); - } - - async getClaimableBalances(address: string) { - return this.compoundClaimableBalanceHelper.getBalances({ - address, - appId, - groupId: COMPOUND_DEFINITION.groups.claimable.id, - network, - }); - } - - async getBalances(address: string) { - const [supplyBalances, borrowBalances, claimableBalances] = await Promise.all([ - this.getSupplyBalances(address), - this.getBorrowBalances(address), - this.getClaimableBalances(address), - ]); - - const meta = this.compoundLendingMetaHelper.getMeta({ balances: [...supplyBalances, ...borrowBalances] }); - const claimableProduct = { label: 'Claimable', assets: claimableBalances }; - const lendingProduct = { label: 'Lending', assets: [...supplyBalances, ...borrowBalances], meta }; - - return presentBalanceFetcherResponse([lendingProduct, claimableProduct]); - } -} diff --git a/src/apps/compound/ethereum/compound.borrow.contract-position-fetcher.ts b/src/apps/compound/ethereum/compound.borrow.contract-position-fetcher.ts index c3a0d707d..db2d1db34 100644 --- a/src/apps/compound/ethereum/compound.borrow.contract-position-fetcher.ts +++ b/src/apps/compound/ethereum/compound.borrow.contract-position-fetcher.ts @@ -1,30 +1,66 @@ -import { Inject } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; -import { Register } from '~app-toolkit/decorators'; -import { PositionFetcher } from '~position/position-fetcher.interface'; -import { ContractPosition } from '~position/position.interface'; +import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface'; import { Network } from '~types/network.interface'; +import { CompoundBorrowContractPositionFetcher } from '../common/compound.borrow.contract-position-fetcher'; import { COMPOUND_DEFINITION } from '../compound.definition'; -import { CompoundBorrowContractPositionHelper } from '../helper/compound.borrow.contract-position-helper'; +import { CompoundComptroller, CompoundContractFactory, CompoundCToken } from '../contracts'; -const appId = COMPOUND_DEFINITION.id; -const groupId = COMPOUND_DEFINITION.groups.borrow.id; -const network = Network.ETHEREUM_MAINNET; +@Injectable() +export class EthereumCompoundBorrowContractPositionFetcher extends CompoundBorrowContractPositionFetcher< + CompoundCToken, + CompoundComptroller +> { + appId = COMPOUND_DEFINITION.id; + groupId = COMPOUND_DEFINITION.groups.borrow.id; + network = Network.ETHEREUM_MAINNET; + groupLabel = 'Lending'; + comptrollerAddress = '0x3d9819210a31b4961b30ef54be2aed79b9c9cd3b'; -@Register.ContractPositionFetcher({ appId, groupId, network }) -export class EthereumCompoundBorrowContractPositionFetcher implements PositionFetcher { constructor( - @Inject(CompoundBorrowContractPositionHelper) - private readonly compoundBorrowContractPositionHelper: CompoundBorrowContractPositionHelper, - ) {} - - async getPositions() { - return this.compoundBorrowContractPositionHelper.getPositions({ - network, - appId, - groupId, - supplyGroupId: COMPOUND_DEFINITION.groups.supply.id, - }); + @Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit, + @Inject(CompoundContractFactory) protected readonly contractFactory: CompoundContractFactory, + ) { + super(appToolkit); + } + + getCompoundCTokenContract(address: string) { + return this.contractFactory.compoundCToken({ address, network: this.network }); + } + + getCompoundComptrollerContract(address: string) { + return this.contractFactory.compoundComptroller({ address, network: this.network }); + } + getMarkets(contract: CompoundComptroller) { + return contract.getAllMarkets(); + } + + async getUnderlyingAddress(contract: CompoundCToken) { + return contract.underlying(); + } + + getExchangeRate(contract: CompoundCToken) { + return contract.exchangeRateCurrent(); + } + + async getBorrowRate(contract: CompoundCToken) { + return contract.borrowRatePerBlock().catch(() => 0); + } + + getCTokenSupply(contract: CompoundCToken) { + return contract.totalSupply(); + } + + getCTokenDecimals(contract: CompoundCToken) { + return contract.decimals(); + } + + getBorrowBalance({ address, contract }: { address: string; contract: CompoundCToken }) { + return contract.borrowBalanceCurrent(address); + } + + getCash(contract: CompoundCToken) { + return contract.getCash(); } } diff --git a/src/apps/compound/ethereum/compound.claimable.contract-position-fetcher.ts b/src/apps/compound/ethereum/compound.claimable.contract-position-fetcher.ts index 6cdd7f120..0a63249f2 100644 --- a/src/apps/compound/ethereum/compound.claimable.contract-position-fetcher.ts +++ b/src/apps/compound/ethereum/compound.claimable.contract-position-fetcher.ts @@ -1,32 +1,19 @@ -import { Inject } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; -import { Register } from '~app-toolkit/decorators'; -import { PositionFetcher } from '~position/position-fetcher.interface'; -import { ContractPosition } from '~position/position.interface'; import { Network } from '~types/network.interface'; +import { CompoundClaimableContractPositionFetcher } from '../common/compound.claimable.contract-position-fetcher'; import { COMPOUND_DEFINITION } from '../compound.definition'; -import { CompoundClaimableContractPositionHelper } from '../helper/compound.claimable.contract-position-helper'; -const appId = COMPOUND_DEFINITION.id; -const groupId = COMPOUND_DEFINITION.groups.claimable.id; -const network = Network.ETHEREUM_MAINNET; +@Injectable() +export class EthereumCompoundClaimableContractPositionFetcher extends CompoundClaimableContractPositionFetcher { + appId = COMPOUND_DEFINITION.id; + groupId = COMPOUND_DEFINITION.groups.claimable.id; + network = Network.ETHEREUM_MAINNET; + groupLabel = 'Claimable'; + isExcludedFromExplore = true; -@Register.ContractPositionFetcher({ appId, groupId, network }) -export class EthereumCompoundClaimableContractPositionFetcher implements PositionFetcher { - constructor( - @Inject(CompoundClaimableContractPositionHelper) - private readonly compoundClaimableContractPositionHelper: CompoundClaimableContractPositionHelper, - ) {} - - async getPositions() { - return this.compoundClaimableContractPositionHelper.getPositions({ - network, - appId, - groupId, - lensAddress: '0xd513d22422a3062bd342ae374b4b9c20e0a9a074', - rewardTokenAddress: '0xc00e94cb662c3520282e6f5717214004a7f26888', - comptrollerAddress: '0x3d9819210a31b4961b30ef54be2aed79b9c9cd3b', - }); - } + lensAddress = '0xd513d22422a3062bd342ae374b4b9c20e0a9a074'; + rewardTokenAddress = '0xc00e94cb662c3520282e6f5717214004a7f26888'; + comptrollerAddress = '0x3d9819210a31b4961b30ef54be2aed79b9c9cd3b'; } diff --git a/src/apps/compound/ethereum/compound.position-presenter.ts b/src/apps/compound/ethereum/compound.position-presenter.ts new file mode 100644 index 000000000..f27c8fd28 --- /dev/null +++ b/src/apps/compound/ethereum/compound.position-presenter.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; + +import { Network } from '~types'; + +import { CompoundPositionPresenter } from '../common/compound.position-presenter'; +import { COMPOUND_DEFINITION } from '../compound.definition'; + +@Injectable() +export class EthereumCompoundPositionPresenter extends CompoundPositionPresenter { + appId = COMPOUND_DEFINITION.id; + network = Network.ETHEREUM_MAINNET; +} diff --git a/src/apps/compound/ethereum/compound.supply.token-fetcher.ts b/src/apps/compound/ethereum/compound.supply.token-fetcher.ts index 33aafc6a7..6f049ed5b 100644 --- a/src/apps/compound/ethereum/compound.supply.token-fetcher.ts +++ b/src/apps/compound/ethereum/compound.supply.token-fetcher.ts @@ -1,40 +1,51 @@ -import { Inject } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; -import { Register } from '~app-toolkit/decorators'; -import { PositionFetcher } from '~position/position-fetcher.interface'; -import { AppTokenPosition } from '~position/position.interface'; +import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface'; import { Network } from '~types/network.interface'; +import { CompoundSupplyTokenFetcher } from '../common/compound.supply.token-fetcher'; import { COMPOUND_DEFINITION } from '../compound.definition'; -import { CompoundContractFactory } from '../contracts'; -import { CompoundSupplyTokenHelper } from '../helper/compound.supply.token-helper'; +import { CompoundComptroller, CompoundContractFactory, CompoundCToken } from '../contracts'; -const appId = COMPOUND_DEFINITION.id; -const groupId = COMPOUND_DEFINITION.groups.supply.id; -const network = Network.ETHEREUM_MAINNET; +@Injectable() +export class EthereumCompoundSupplyTokenFetcher extends CompoundSupplyTokenFetcher< + CompoundCToken, + CompoundComptroller +> { + appId = COMPOUND_DEFINITION.id; + groupId = COMPOUND_DEFINITION.groups.supply.id; + network = Network.ETHEREUM_MAINNET; + groupLabel = 'Lending'; + comptrollerAddress = '0x3d9819210a31b4961b30ef54be2aed79b9c9cd3b'; -@Register.TokenPositionFetcher({ appId, groupId, network }) -export class EthereumCompoundSupplyTokenFetcher implements PositionFetcher { constructor( - @Inject(CompoundContractFactory) private readonly compoundContractFactory: CompoundContractFactory, - @Inject(CompoundSupplyTokenHelper) private readonly compoundSupplyTokenHelper: CompoundSupplyTokenHelper, - ) {} - - async getPositions() { - return this.compoundSupplyTokenHelper.getTokens({ - network, - appId, - groupId, - comptrollerAddress: '0x3d9819210a31b4961b30ef54be2aed79b9c9cd3b', - getComptrollerContract: ({ address, network }) => - this.compoundContractFactory.compoundComptroller({ address, network }), - getTokenContract: ({ address, network }) => this.compoundContractFactory.compoundCToken({ address, network }), - getAllMarkets: ({ contract, multicall }) => multicall.wrap(contract).getAllMarkets(), - getExchangeRate: ({ contract, multicall }) => multicall.wrap(contract).exchangeRateCurrent(), - getSupplyRate: ({ contract, multicall }) => multicall.wrap(contract).supplyRatePerBlock(), - getBorrowRate: ({ contract, multicall }) => multicall.wrap(contract).borrowRatePerBlock(), - getUnderlyingAddress: ({ contract, multicall }) => multicall.wrap(contract).underlying(), - getExchangeRateMantissa: ({ underlyingTokenDecimals }) => underlyingTokenDecimals + 10, - }); + @Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit, + @Inject(CompoundContractFactory) protected readonly contractFactory: CompoundContractFactory, + ) { + super(appToolkit); + } + + getCompoundCTokenContract(address: string) { + return this.contractFactory.compoundCToken({ address, network: this.network }); + } + + getCompoundComptrollerContract(address: string) { + return this.contractFactory.compoundComptroller({ address, network: this.network }); + } + + getMarkets(contract: CompoundComptroller) { + return contract.getAllMarkets(); + } + + async getUnderlyingAddress(contract: CompoundCToken) { + return contract.underlying(); + } + + getExchangeRate(contract: CompoundCToken) { + return contract.exchangeRateCurrent(); + } + + async getSupplyRate(contract: CompoundCToken) { + return contract.supplyRatePerBlock().catch(() => 0); } } diff --git a/src/apps/pika-protocol-v3/optimism/pika-protocol-v3.balance-fetcher.ts b/src/apps/pika-protocol-v3/optimism/pika-protocol-v3.balance-fetcher.ts index fb1a06c2f..e9433858a 100644 --- a/src/apps/pika-protocol-v3/optimism/pika-protocol-v3.balance-fetcher.ts +++ b/src/apps/pika-protocol-v3/optimism/pika-protocol-v3.balance-fetcher.ts @@ -19,7 +19,7 @@ export class OptimismPikaProtocolV3BalanceFetcher implements BalanceFetcher { constructor( @Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit, @Inject(PikaProtocolV3ContractFactory) private readonly contractFactory: PikaProtocolV3ContractFactory, - ) { } + ) {} async getBalances(address: string) { const [vaultBalances] = await Promise.all([this.getFarmBalances(address)]); @@ -31,7 +31,7 @@ export class OptimismPikaProtocolV3BalanceFetcher implements BalanceFetcher { const userShare = await contractInst.getShare(address); const vaultBalance = (await contractInst.getVault()).balance; const totalShare = await contractInst.getTotalShare(); - return Number(userShare) * Number(vaultBalance) / Number(totalShare); + return (Number(userShare) * Number(vaultBalance)) / Number(totalShare); } async getFarmBalances(address: string) {