This repository has been archived by the owner on Jan 24, 2024. It is now read-only.
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(compound): Migrate to template (#1370)
- Loading branch information
Showing
11 changed files
with
527 additions
and
158 deletions.
There are no files selected for viewing
153 changes: 153 additions & 0 deletions
153
src/apps/compound/common/compound.borrow.contract-position-fetcher.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<R, CompoundBorrowTokenDataProps> { | ||
abstract comptrollerAddress: string; | ||
abstract getCompoundCTokenContract(address: string): R; | ||
abstract getCompoundComptrollerContract(address: string): S; | ||
|
||
abstract getMarkets(contract: S): Promise<string[]>; | ||
abstract getUnderlyingAddress(contract: R): Promise<string>; | ||
abstract getExchangeRate(contract: R): Promise<BigNumberish>; | ||
abstract getBorrowRate(contract: R): Promise<BigNumberish>; | ||
abstract getCash(contract: R): Promise<BigNumberish>; | ||
abstract getBorrowBalance(opts: { address: string; contract: R }): Promise<BigNumberish>; | ||
abstract getCTokenSupply(contract: R): Promise<BigNumberish>; | ||
abstract getCTokenDecimals(contract: R): Promise<number>; | ||
|
||
getContract(address: string): R { | ||
return this.getCompoundCTokenContract(address); | ||
} | ||
|
||
async getDefinitions({ multicall }: GetDefinitionsParams): Promise<DefaultContractPositionDefinition[]> { | ||
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<R, DefaultContractPositionDefinition>): Promise<UnderlyingTokenDefinition[] | null> { | ||
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<R, CompoundBorrowTokenDataProps>): Promise<DisplayProps['label']> { | ||
const [underlyingToken] = contractPosition.tokens; | ||
return getLabelFromToken(underlyingToken); | ||
} | ||
|
||
async getSecondaryLabel({ | ||
contractPosition, | ||
}: GetDisplayPropsParams<R, CompoundBorrowTokenDataProps>): Promise<DisplayProps['secondaryLabel']> { | ||
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<R, CompoundBorrowTokenDataProps>) { | ||
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<R, CompoundBorrowTokenDataProps>) { | ||
const [underlyingToken] = contractPosition.tokens; | ||
const rateRaw = await this.getExchangeRate(contract); | ||
const mantissa = underlyingToken.decimals + 10; | ||
|
||
return Number(rateRaw) / 10 ** mantissa; | ||
} | ||
|
||
async getDataProps(opts: GetDataPropsParams<R, CompoundBorrowTokenDataProps>): Promise<CompoundBorrowTokenDataProps> { | ||
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<R, CompoundBorrowTokenDataProps>) { | ||
const balanceRaw = await this.getBorrowBalance({ contract, address }); | ||
return [balanceRaw]; | ||
} | ||
} |
82 changes: 82 additions & 0 deletions
82
src/apps/compound/common/compound.claimable.contract-position-fetcher.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<CompoundComptroller> { | ||
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<DefaultContractPositionDefinition[]> { | ||
return [{ address: this.comptrollerAddress }]; | ||
} | ||
|
||
async getTokenDefinitions( | ||
_opts: GetTokenDefinitionsParams<CompoundComptroller, DefaultContractPositionDefinition>, | ||
): Promise<UnderlyingTokenDefinition[] | null> { | ||
return [{ address: this.rewardTokenAddress, metaType: MetaType.CLAIMABLE }]; | ||
} | ||
|
||
async getLabel({ contractPosition }: GetDisplayPropsParams<CompoundComptroller>) { | ||
const rewardToken = contractPosition.tokens[0]; | ||
return `Claimable ${getLabelFromToken(rewardToken)}`; | ||
} | ||
|
||
async getDataProps( | ||
_params: GetDataPropsParams<CompoundComptroller, CompoundClaimablePositionDataProps>, | ||
): Promise<CompoundClaimablePositionDataProps> { | ||
return { | ||
lensAddress: this.lensAddress, | ||
}; | ||
} | ||
|
||
async getTokenBalancesPerPosition({ | ||
address, | ||
contractPosition, | ||
}: GetTokenBalancesParams<CompoundComptroller, CompoundClaimablePositionDataProps>): Promise<BigNumberish[]> { | ||
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]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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', | ||
}, | ||
]; | ||
} | ||
} |
Oops, something went wrong.