From 7e97ef89e172b8be416ae0d072aab0612d9c0b53 Mon Sep 17 00:00:00 2001 From: immasandwich Date: Mon, 28 Nov 2022 18:51:04 -0500 Subject: [PATCH] feat(saddle): Migrate to templates, add Curve template for simple static definitions (#1813) * feat(saddle): Migrate to templates, add Curve template for simple static definitions * Gr * feat(saddle): Smol fixes --- .../common/curve.crypto-pool.token-fetcher.ts | 6 +- ...curve.factory-crypto-pool.token-fetcher.ts | 6 +- ...curve.factory-stable-pool.token-fetcher.ts | 6 +- ...ts => curve.pool-dynamic.token-fetcher.ts} | 2 +- .../common/curve.pool-static.token-fetcher.ts | 160 ++++++++++++++++++ .../common/curve.stable-pool.token-fetcher.ts | 6 +- .../saddle/ethereum/saddle.balance-fetcher.ts | 91 ---------- ...communal-farm.contract-position-fetcher.ts | 79 ++++----- ...-chef-v2-farm.contract-position-fetcher.ts | 158 ++++++++++------- .../ethereum/saddle.pool.definitions.ts | 24 ++- .../ethereum/saddle.pool.token-fetcher.ts | 78 ++++----- src/apps/saddle/saddle.module.ts | 15 -- ...chef.template.contract-position-fetcher.ts | 3 +- 13 files changed, 346 insertions(+), 288 deletions(-) rename src/apps/curve/common/{curve.pool.token-fetcher.ts => curve.pool-dynamic.token-fetcher.ts} (98%) create mode 100644 src/apps/curve/common/curve.pool-static.token-fetcher.ts delete mode 100644 src/apps/saddle/ethereum/saddle.balance-fetcher.ts diff --git a/src/apps/curve/common/curve.crypto-pool.token-fetcher.ts b/src/apps/curve/common/curve.crypto-pool.token-fetcher.ts index 46df190e1..ac8bb8a98 100644 --- a/src/apps/curve/common/curve.crypto-pool.token-fetcher.ts +++ b/src/apps/curve/common/curve.crypto-pool.token-fetcher.ts @@ -5,17 +5,17 @@ import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface'; import { CurveContractFactory, CurveCryptoRegistry } from '../contracts'; import { - CurvePoolTokenFetcher, + CurvePoolDynamicTokenFetcher, ResolveCoinAddressesParams, ResolveFeesParams, ResolvePoolCountParams, ResolveReservesParams, ResolveSwapAddressParams, ResolveTokenAddressParams, -} from './curve.pool.token-fetcher'; +} from './curve.pool-dynamic.token-fetcher'; import { CurveVolumeDataLoader } from './curve.volume.data-loader'; -export abstract class CurveCryptoPoolTokenFetcher extends CurvePoolTokenFetcher { +export abstract class CurveCryptoPoolTokenFetcher extends CurvePoolDynamicTokenFetcher { constructor( @Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit, @Inject(CurveContractFactory) protected readonly contractFactory: CurveContractFactory, diff --git a/src/apps/curve/common/curve.factory-crypto-pool.token-fetcher.ts b/src/apps/curve/common/curve.factory-crypto-pool.token-fetcher.ts index c69dfbab8..2406ac1cf 100644 --- a/src/apps/curve/common/curve.factory-crypto-pool.token-fetcher.ts +++ b/src/apps/curve/common/curve.factory-crypto-pool.token-fetcher.ts @@ -6,17 +6,17 @@ import { isMulticallUnderlyingError } from '~multicall/multicall.ethers'; import { CurveContractFactory, CurveCryptoFactory } from '../contracts'; import { - CurvePoolTokenFetcher, + CurvePoolDynamicTokenFetcher, ResolveCoinAddressesParams, ResolveFeesParams, ResolvePoolCountParams, ResolveReservesParams, ResolveSwapAddressParams, ResolveTokenAddressParams, -} from './curve.pool.token-fetcher'; +} from './curve.pool-dynamic.token-fetcher'; import { CurveVolumeDataLoader } from './curve.volume.data-loader'; -export abstract class CurveFactoryCryptoPoolTokenFetcher extends CurvePoolTokenFetcher { +export abstract class CurveFactoryCryptoPoolTokenFetcher extends CurvePoolDynamicTokenFetcher { constructor( @Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit, @Inject(CurveContractFactory) protected readonly contractFactory: CurveContractFactory, diff --git a/src/apps/curve/common/curve.factory-stable-pool.token-fetcher.ts b/src/apps/curve/common/curve.factory-stable-pool.token-fetcher.ts index f3607b86f..7aa7b647d 100644 --- a/src/apps/curve/common/curve.factory-stable-pool.token-fetcher.ts +++ b/src/apps/curve/common/curve.factory-stable-pool.token-fetcher.ts @@ -5,17 +5,17 @@ import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface'; import { CurveContractFactory, CurveStableFactory } from '../contracts'; import { - CurvePoolTokenFetcher, + CurvePoolDynamicTokenFetcher, ResolveCoinAddressesParams, ResolveFeesParams, ResolvePoolCountParams, ResolveReservesParams, ResolveSwapAddressParams, ResolveTokenAddressParams, -} from './curve.pool.token-fetcher'; +} from './curve.pool-dynamic.token-fetcher'; import { CurveVolumeDataLoader } from './curve.volume.data-loader'; -export abstract class CurveFactoryStablePoolTokenFetcher extends CurvePoolTokenFetcher { +export abstract class CurveFactoryStablePoolTokenFetcher extends CurvePoolDynamicTokenFetcher { constructor( @Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit, @Inject(CurveContractFactory) protected readonly contractFactory: CurveContractFactory, diff --git a/src/apps/curve/common/curve.pool.token-fetcher.ts b/src/apps/curve/common/curve.pool-dynamic.token-fetcher.ts similarity index 98% rename from src/apps/curve/common/curve.pool.token-fetcher.ts rename to src/apps/curve/common/curve.pool-dynamic.token-fetcher.ts index 3bc47a164..f3130647c 100644 --- a/src/apps/curve/common/curve.pool.token-fetcher.ts +++ b/src/apps/curve/common/curve.pool-dynamic.token-fetcher.ts @@ -76,7 +76,7 @@ export type ResolveFeesParams = { multicall: IMulticallWrapper; }; -export abstract class CurvePoolTokenFetcher extends AppTokenTemplatePositionFetcher< +export abstract class CurvePoolDynamicTokenFetcher extends AppTokenTemplatePositionFetcher< Erc20, CurvePoolTokenDataProps, CurvePoolDefinition diff --git a/src/apps/curve/common/curve.pool-static.token-fetcher.ts b/src/apps/curve/common/curve.pool-static.token-fetcher.ts new file mode 100644 index 000000000..47e5eb287 --- /dev/null +++ b/src/apps/curve/common/curve.pool-static.token-fetcher.ts @@ -0,0 +1,160 @@ +import { Inject } from '@nestjs/common'; +import { BigNumberish, Contract } from 'ethers'; +import { compact, range } from 'lodash'; + +import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface'; +import { ETH_ADDR_ALIAS, ZERO_ADDRESS } from '~app-toolkit/constants/address'; +import { + buildDollarDisplayItem, + buildPercentageDisplayItem, +} from '~app-toolkit/helpers/presentation/display-item.present'; +import { getLabelFromToken } from '~app-toolkit/helpers/presentation/image.present'; +import { ContractFactory, Erc20 } from '~contract/contracts'; +import { IMulticallWrapper } from '~multicall'; +import { isMulticallUnderlyingError } from '~multicall/multicall.ethers'; +import { AppTokenTemplatePositionFetcher } from '~position/template/app-token.template.position-fetcher'; +import { + GetAddressesParams, + DefaultAppTokenDefinition, + GetUnderlyingTokensParams, + GetPricePerShareParams, + GetDataPropsParams, + GetDisplayPropsParams, +} from '~position/template/app-token.template.types'; + +export type CurvePoolTokenDataProps = { + swapAddress: string; + liquidity: number; + reserves: number[]; + apy: number; + volume: number; + fee: number; +}; + +export type CurvePoolDefinition = { + address: string; + swapAddress: string; + isLegacy?: boolean; +}; + +export type ResolvePoolCoinAddressParams = { + contract: T; + multicall: IMulticallWrapper; + index: number; +}; + +export type ResolvePoolReserveParams = { + contract: T; + multicall: IMulticallWrapper; + index: number; +}; + +export type ResolvePoolFeeParams = { + contract: T; + multicall: IMulticallWrapper; +}; + +export abstract class CurvePoolStaticTokenFetcher extends AppTokenTemplatePositionFetcher< + Erc20, + CurvePoolTokenDataProps, + CurvePoolDefinition +> { + abstract poolDefinitions: CurvePoolDefinition[]; + + abstract resolvePoolContract(definition: CurvePoolDefinition): T; + abstract resolvePoolCoinAddress(opts: ResolvePoolCoinAddressParams): Promise; + abstract resolvePoolReserve(opts: ResolvePoolReserveParams): Promise; + abstract resolvePoolFee(opts: ResolvePoolFeeParams): Promise; + + constructor( + @Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit, + @Inject(ContractFactory) protected readonly contractFactory: ContractFactory, + ) { + super(appToolkit); + } + + getContract(address: string): Erc20 { + return this.contractFactory.erc20({ address, network: this.network }); + } + + async getDefinitions() { + return this.poolDefinitions; + } + + async getAddresses({ definitions }: GetAddressesParams) { + return definitions.map(v => v.address); + } + + async getUnderlyingTokenAddresses({ multicall, definition }: GetUnderlyingTokensParams) { + const contract = multicall.wrap(this.resolvePoolContract(definition)); + + const coinAddressesRaw = await Promise.all( + range(0, 4).map(index => + this.resolvePoolCoinAddress({ multicall, contract, index }).catch(err => { + if (isMulticallUnderlyingError(err)) return null; + throw err; + }), + ), + ); + + return compact(coinAddressesRaw) + .filter(v => v !== ZERO_ADDRESS) + .map(v => v.toLowerCase()) + .map(v => v.replace(ETH_ADDR_ALIAS, ZERO_ADDRESS)); + } + + async getPricePerShare({ + multicall, + definition, + appToken, + }: GetPricePerShareParams) { + const contract = multicall.wrap(this.resolvePoolContract(definition)); + const coinsRange = range(0, appToken.tokens.length); + const reservesRaw = await Promise.all( + coinsRange.map(index => this.resolvePoolReserve({ multicall, contract, index })), + ); + + const reserves = reservesRaw.map((v, i) => Number(v) / 10 ** appToken.tokens[i].decimals); + const pricePerShare = reserves.map(v => v / appToken.supply); + return pricePerShare; + } + + async getDataProps(params: GetDataPropsParams) { + const defaultDataProps = await super.getDataProps(params); + + const { multicall, definition } = params; + const contract = multicall.wrap(this.resolvePoolContract(definition)); + const swapAddress = definition.swapAddress; + + const fees = await this.resolvePoolFee({ contract, multicall }); + const fee = Number(fees[0]) / 10 ** 8; + const volume = 0; // not supported + const apy = 0; // not supported + + return { ...defaultDataProps, fee, volume, apy, swapAddress }; + } + + async getLabel({ appToken }: GetDisplayPropsParams) { + return appToken.tokens.map(v => getLabelFromToken(v)).join(' / '); + } + + async getSecondaryLabel({ appToken }: GetDisplayPropsParams) { + const reservesUSD = appToken.tokens.map((t, i) => appToken.dataProps.reserves[i] * t.price); + const liquidity = reservesUSD.reduce((total, r) => total + r, 0); + const reservePercentages = reservesUSD.map(reserveUSD => reserveUSD / liquidity); + const ratio = reservePercentages.map(p => `${Math.floor(p * 100)}%`).join(' / '); + + return ratio; + } + + async getStatsItems({ appToken }: GetDisplayPropsParams) { + const { liquidity, volume, apy, fee } = appToken.dataProps; + + return [ + { label: 'Liquidity', value: buildDollarDisplayItem(liquidity) }, + { label: 'Volume', value: buildDollarDisplayItem(volume) }, + { label: 'APY', value: buildPercentageDisplayItem(apy) }, + { label: 'Fee', value: buildPercentageDisplayItem(fee) }, + ]; + } +} diff --git a/src/apps/curve/common/curve.stable-pool.token-fetcher.ts b/src/apps/curve/common/curve.stable-pool.token-fetcher.ts index 235ac89ce..4b010659e 100644 --- a/src/apps/curve/common/curve.stable-pool.token-fetcher.ts +++ b/src/apps/curve/common/curve.stable-pool.token-fetcher.ts @@ -5,17 +5,17 @@ import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface'; import { CurveContractFactory, CurveStableRegistry } from '../contracts'; import { - CurvePoolTokenFetcher, + CurvePoolDynamicTokenFetcher, ResolveCoinAddressesParams, ResolveFeesParams, ResolvePoolCountParams, ResolveReservesParams, ResolveSwapAddressParams, ResolveTokenAddressParams, -} from './curve.pool.token-fetcher'; +} from './curve.pool-dynamic.token-fetcher'; import { CurveVolumeDataLoader } from './curve.volume.data-loader'; -export abstract class CurveStablePoolTokenFetcher extends CurvePoolTokenFetcher { +export abstract class CurveStablePoolTokenFetcher extends CurvePoolDynamicTokenFetcher { constructor( @Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit, @Inject(CurveContractFactory) protected readonly contractFactory: CurveContractFactory, diff --git a/src/apps/saddle/ethereum/saddle.balance-fetcher.ts b/src/apps/saddle/ethereum/saddle.balance-fetcher.ts deleted file mode 100644 index caa073ec4..000000000 --- a/src/apps/saddle/ethereum/saddle.balance-fetcher.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Inject } from '@nestjs/common'; - -import { drillBalance } from '~app-toolkit'; -import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface'; -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 { MetaType } from '~position/position.interface'; -import { Network } from '~types/network.interface'; - -import { SaddleCommunalFarm, SaddleContractFactory, SaddleMiniChefV2 } from '../contracts'; -import { SADDLE_DEFINITION } from '../saddle.definition'; - -@Register.BalanceFetcher(SADDLE_DEFINITION.id, Network.ETHEREUM_MAINNET) -export class EthereumSaddleBalanceFetcher implements BalanceFetcher { - constructor( - @Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit, - @Inject(SaddleContractFactory) private readonly contractFactory: SaddleContractFactory, - ) {} - - private async getPoolTokenBalances(address: string) { - return this.appToolkit.helpers.tokenBalanceHelper.getTokenBalances({ - address, - appId: SADDLE_DEFINITION.id, - groupId: SADDLE_DEFINITION.groups.pool.id, - network: Network.ETHEREUM_MAINNET, - }); - } - - private async getCommunalFarmBalances(address: string) { - return this.appToolkit.helpers.singleStakingContractPositionBalanceHelper.getBalances({ - address, - appId: SADDLE_DEFINITION.id, - groupId: SADDLE_DEFINITION.groups.communalFarm.id, - network: Network.ETHEREUM_MAINNET, - resolveContract: ({ address, network }) => this.contractFactory.saddleCommunalFarm({ address, network }), - resolveStakedTokenBalance: ({ contract, address, multicall }) => - multicall.wrap(contract).lockedLiquidityOf(address), - resolveRewardTokenBalances: ({ contract, address, multicall }) => multicall.wrap(contract).earned(address), - }); - } - - private async getMiniChefV2FarmBalances(address: string) { - return this.appToolkit.helpers.masterChefContractPositionBalanceHelper.getBalances({ - address, - appId: SADDLE_DEFINITION.id, - groupId: SADDLE_DEFINITION.groups.miniChefV2.id, - network: Network.ETHEREUM_MAINNET, - resolveChefContract: ({ contractAddress }) => - this.contractFactory.saddleMiniChefV2({ network: Network.ETHEREUM_MAINNET, address: contractAddress }), - resolveStakedTokenBalance: this.appToolkit.helpers.masterChefDefaultStakedBalanceStrategy.build({ - resolveStakedBalance: ({ contract, multicall, contractPosition }) => - multicall - .wrap(contract) - .userInfo(contractPosition.dataProps.poolIndex, address) - .then(v => v.amount), - }), - resolveClaimableTokenBalances: async ({ address, contract, contractPosition, multicall }) => { - const pendingTokens = await multicall - .wrap(contract) - .pendingSaddle(contractPosition.dataProps.poolIndex, address); - - const claimableToken = contractPosition.tokens.find(t => t.metaType === MetaType.CLAIMABLE)!; - return [drillBalance(claimableToken, pendingTokens.toString())]; - }, - }); - } - - async getBalances(address: string) { - const [poolTokenBalances, communalFarmBalances, miniChefV2FarmBalances] = await Promise.all([ - this.getPoolTokenBalances(address), - this.getCommunalFarmBalances(address), - this.getMiniChefV2FarmBalances(address), - ]); - - return presentBalanceFetcherResponse([ - { - label: 'Pools', - assets: poolTokenBalances, - }, - { - label: 'Farms', - assets: miniChefV2FarmBalances, - }, - { - label: 'Communal Farms', - assets: communalFarmBalances, - }, - ]); - } -} diff --git a/src/apps/saddle/ethereum/saddle.communal-farm.contract-position-fetcher.ts b/src/apps/saddle/ethereum/saddle.communal-farm.contract-position-fetcher.ts index b173d7abe..d9878db42 100644 --- a/src/apps/saddle/ethereum/saddle.communal-farm.contract-position-fetcher.ts +++ b/src/apps/saddle/ethereum/saddle.communal-farm.contract-position-fetcher.ts @@ -1,14 +1,11 @@ import { Inject } from '@nestjs/common'; import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface'; -import { Register } from '~app-toolkit/decorators'; -import { SynthetixSingleStakingIsActiveStrategy, SynthetixSingleStakingRoiStrategy } from '~apps/synthetix'; -import { PositionFetcher } from '~position/position-fetcher.interface'; -import { ContractPosition } from '~position/position.interface'; -import { Network } from '~types/network.interface'; +import { PositionTemplate } from '~app-toolkit/decorators/position-template.decorator'; +import { GetDataPropsParams, GetTokenBalancesParams } from '~position/template/contract-position.template.types'; +import { SingleStakingFarmTemplateContractPositionFetcher } from '~position/template/single-staking.template.contract-position-fetcher'; import { SaddleCommunalFarm, SaddleContractFactory } from '../contracts'; -import SADDLE_DEFINITION from '../saddle.definition'; const FARMS = [ // Saddle D4 Communal Farm @@ -24,47 +21,39 @@ const FARMS = [ }, ]; -const appId = SADDLE_DEFINITION.id; -const groupId = SADDLE_DEFINITION.groups.communalFarm.id; -const network = Network.ETHEREUM_MAINNET; +@PositionTemplate() +export class EthereumSaddleCommunalFarmContractPositionFetcher extends SingleStakingFarmTemplateContractPositionFetcher { + groupLabel = 'Communal Farms'; -@Register.ContractPositionFetcher({ appId, groupId, network }) -export class EthereumSaddleCommunalFarmContractPositionFetcher implements PositionFetcher { constructor( - @Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit, - @Inject(SaddleContractFactory) private readonly contractFactory: SaddleContractFactory, - @Inject(SynthetixSingleStakingIsActiveStrategy) - private readonly synthetixSingleStakingIsActiveStrategy: SynthetixSingleStakingIsActiveStrategy, - @Inject(SynthetixSingleStakingRoiStrategy) - private readonly synthetixSingleStakingRoiStrategy: SynthetixSingleStakingRoiStrategy, - ) {} + @Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit, + @Inject(SaddleContractFactory) protected readonly contractFactory: SaddleContractFactory, + ) { + super(appToolkit); + } + + getContract(address: string): SaddleCommunalFarm { + return this.contractFactory.saddleCommunalFarm({ address, network: this.network }); + } + + async getFarmDefinitions() { + return FARMS; + } + + async getRewardRates({ contract }: GetDataPropsParams) { + return Promise.all([ + contract.rewardRates(0), + contract.rewardRates(1), + contract.rewardRates(2), + contract.rewardRates(3), + ]); + } + + async getStakedTokenBalance({ address, contract }: GetTokenBalancesParams) { + return contract.lockedLiquidityOf(address); + } - async getPositions() { - return this.appToolkit.helpers.singleStakingFarmContractPositionHelper.getContractPositions({ - appId, - groupId, - network, - dependencies: [ - { - appId: SADDLE_DEFINITION.id, - groupIds: [SADDLE_DEFINITION.groups.pool.id], - network: Network.ETHEREUM_MAINNET, - }, - ], - resolveFarmDefinitions: async () => FARMS, - resolveFarmContract: opts => this.contractFactory.saddleCommunalFarm(opts), - resolveIsActive: this.synthetixSingleStakingIsActiveStrategy.build({ - resolvePeriodFinish: ({ contract, multicall }) => multicall.wrap(contract).periodFinish(), - }), - resolveRois: this.synthetixSingleStakingRoiStrategy.build({ - resolveRewardRates: ({ contract, multicall }) => - Promise.all([ - multicall.wrap(contract).rewardRates(0), - multicall.wrap(contract).rewardRates(1), - multicall.wrap(contract).rewardRates(2), - multicall.wrap(contract).rewardRates(3), - ]), - }), - }); + async getRewardTokenBalances({ address, contract }: GetTokenBalancesParams) { + return contract.earned(address); } } diff --git a/src/apps/saddle/ethereum/saddle.mini-chef-v2-farm.contract-position-fetcher.ts b/src/apps/saddle/ethereum/saddle.mini-chef-v2-farm.contract-position-fetcher.ts index 6ecf1d7c1..84a62213b 100644 --- a/src/apps/saddle/ethereum/saddle.mini-chef-v2-farm.contract-position-fetcher.ts +++ b/src/apps/saddle/ethereum/saddle.mini-chef-v2-farm.contract-position-fetcher.ts @@ -1,76 +1,106 @@ import { Inject } from '@nestjs/common'; import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface'; -import { Register } from '~app-toolkit/decorators'; -import { RewardRateUnit } from '~app-toolkit/helpers/master-chef/master-chef.contract-position-helper'; -import { PositionFetcher } from '~position/position-fetcher.interface'; -import { ContractPosition } from '~position/position.interface'; -import { Network } from '~types/network.interface'; +import { ZERO_ADDRESS } from '~app-toolkit/constants/address'; +import { PositionTemplate } from '~app-toolkit/decorators/position-template.decorator'; +import { + GetMasterChefV2ExtraRewardTokenBalancesParams, + GetMasterChefV2ExtraRewardTokenRewardRates, + MasterChefV2TemplateContractPositionFetcher, +} from '~position/template/master-chef-v2.template.contract-position-fetcher'; +import { + GetMasterChefDataPropsParams, + GetMasterChefTokenBalancesParams, +} from '~position/template/master-chef.template.contract-position-fetcher'; import { SaddleContractFactory } from '../contracts'; import { SaddleMiniChefV2 } from '../contracts/ethers/SaddleMiniChefV2'; import { SaddleMiniChefV2Rewarder } from '../contracts/ethers/SaddleMiniChefV2Rewarder'; -import { SADDLE_DEFINITION } from '../saddle.definition'; -const appId = SADDLE_DEFINITION.id; -const groupId = SADDLE_DEFINITION.groups.miniChefV2.id; -const network = Network.ETHEREUM_MAINNET; +@PositionTemplate() +export class EthereumSaddleMiniChefV2FarmContractPositionFetcher extends MasterChefV2TemplateContractPositionFetcher< + SaddleMiniChefV2, + SaddleMiniChefV2Rewarder +> { + groupLabel = 'Farms'; + chefAddress = '0x691ef79e40d909c715be5e9e93738b3ff7d58534'; -@Register.ContractPositionFetcher({ appId, groupId, network }) -export class EthereumSaddleMiniChefV2FarmContractPositionFetcher implements PositionFetcher { constructor( - @Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit, - @Inject(SaddleContractFactory) private readonly saddleContractFactory: SaddleContractFactory, - ) {} - - async getPositions() { - return this.appToolkit.helpers.masterChefContractPositionHelper.getContractPositions({ - address: '0x691ef79e40d909c715be5e9e93738b3ff7d58534', - appId: SADDLE_DEFINITION.id, - groupId: SADDLE_DEFINITION.groups.miniChefV2.id, - network: Network.ETHEREUM_MAINNET, - dependencies: [ - { - appId: SADDLE_DEFINITION.id, - groupIds: [SADDLE_DEFINITION.groups.pool.id], - network: Network.ETHEREUM_MAINNET, - }, - ], - resolveContract: ({ address, network }) => this.saddleContractFactory.saddleMiniChefV2({ address, network }), - resolvePoolLength: ({ multicall, contract }) => multicall.wrap(contract).poolLength(), - resolveDepositTokenAddress: ({ poolIndex, contract, multicall }) => multicall.wrap(contract).lpToken(poolIndex), - resolveRewardTokenAddresses: this.appToolkit.helpers.masterChefV2ClaimableTokenStrategy.build< - SaddleMiniChefV2, - SaddleMiniChefV2Rewarder - >({ - resolvePrimaryClaimableToken: ({ multicall, contract }) => multicall.wrap(contract).SADDLE(), - resolveRewarderAddress: ({ multicall, contract, poolIndex }) => multicall.wrap(contract).rewarder(poolIndex), - resolveRewarderContract: ({ network, rewarderAddress }) => - this.saddleContractFactory.saddleMiniChefV2Rewarder({ address: rewarderAddress, network }), - resolveSecondaryClaimableToken: ({ multicall, rewarderContract }) => - multicall.wrap(rewarderContract).rewardToken(), - }), - rewardRateUnit: RewardRateUnit.SECOND, - resolveRewardRate: this.appToolkit.helpers.masterChefV2RewardRateStrategy.build< - SaddleMiniChefV2, - SaddleMiniChefV2Rewarder - >({ - resolvePoolAllocPoints: async ({ poolIndex, contract, multicall }) => - multicall - .wrap(contract) - .poolInfo(poolIndex) - .then(v => v.allocPoint), - resolveTotalAllocPoints: ({ multicall, contract }) => multicall.wrap(contract).totalAllocPoint(), - resolvePrimaryTotalRewardRate: async ({ multicall, contract }) => multicall.wrap(contract).saddlePerSecond(), - resolveRewarderAddress: ({ multicall, contract, poolIndex }) => multicall.wrap(contract).rewarder(poolIndex), - resolveRewarderContract: ({ network, rewarderAddress }) => - this.saddleContractFactory.saddleMiniChefV2Rewarder({ address: rewarderAddress, network }), - resolveSecondaryTotalRewardRate: async ({ multicall, rewarderContract }) => - multicall - .wrap(rewarderContract) - .rewardPerSecond() - .catch(() => '0'), - }), - }); + @Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit, + @Inject(SaddleContractFactory) protected readonly contractFactory: SaddleContractFactory, + ) { + super(appToolkit); + } + + getContract(address: string): SaddleMiniChefV2 { + return this.contractFactory.saddleMiniChefV2({ address, network: this.network }); + } + + getExtraRewarderContract(address: string) { + return this.contractFactory.saddleMiniChefV2Rewarder({ address, network: this.network }); + } + + async getPoolLength(contract: SaddleMiniChefV2) { + return contract.poolLength(); + } + + async getStakedTokenAddress(contract: SaddleMiniChefV2, poolIndex: number) { + return contract.lpToken(poolIndex); + } + + async getRewardTokenAddress(contract: SaddleMiniChefV2) { + return contract.SADDLE(); + } + + async getExtraRewarder(contract: SaddleMiniChefV2, poolIndex: number) { + return contract.rewarder(poolIndex); + } + + async getExtraRewardTokenAddresses(contract: SaddleMiniChefV2Rewarder, poolIndex: number) { + return contract.pendingTokens(poolIndex, ZERO_ADDRESS, 0).then(v => [v.rewardTokens[0]]); + } + + async getTotalAllocPoints({ contract }: GetMasterChefDataPropsParams) { + return contract.totalAllocPoint(); + } + + async getTotalRewardRate({ contract }: GetMasterChefDataPropsParams) { + return contract.saddlePerSecond(); + } + + async getPoolAllocPoints({ contract, definition }: GetMasterChefDataPropsParams) { + return contract.poolInfo(definition.poolIndex).then(v => v.allocPoint); + } + + async getExtraRewardTokenRewardRates({ + rewarderContract, + }: GetMasterChefV2ExtraRewardTokenRewardRates) { + return rewarderContract.rewardPerSecond().catch(_err => 0); + } + + async getStakedTokenBalance({ + address, + contract, + contractPosition, + }: GetMasterChefTokenBalancesParams) { + return contract.userInfo(contractPosition.dataProps.poolIndex, address).then(v => v.amount); + } + + async getRewardTokenBalance({ + address, + contract, + contractPosition, + }: GetMasterChefTokenBalancesParams) { + return contract.pendingSaddle(contractPosition.dataProps.poolIndex, address); + } + + async getExtraRewardTokenBalances({ + address, + rewarderContract, + contractPosition, + }: GetMasterChefV2ExtraRewardTokenBalancesParams) { + return rewarderContract + .pendingTokens(contractPosition.dataProps.poolIndex, address, 0) + .then(v => v.rewardAmounts[0]); } } diff --git a/src/apps/saddle/ethereum/saddle.pool.definitions.ts b/src/apps/saddle/ethereum/saddle.pool.definitions.ts index 59ecbecd0..f81a9195e 100644 --- a/src/apps/saddle/ethereum/saddle.pool.definitions.ts +++ b/src/apps/saddle/ethereum/saddle.pool.definitions.ts @@ -1,54 +1,52 @@ -import { CurvePoolDefinition } from '~apps/curve/curve.types'; - -export const SADDLE_POOL_DEFINITIONS: CurvePoolDefinition[] = [ +export const SADDLE_POOL_DEFINITIONS = [ // Saddle tBTC/wBTC/renBTC/sBTC (BTC Pool v1) { + address: '0xc28df698475dec994be00c9c9d8658a548e6304f', swapAddress: '0x4f6a43ad7cba042606decaca730d4ce0a57ac62e', - tokenAddress: '0xc28df698475dec994be00c9c9d8658a548e6304f', }, // Saddle wBTC/renBTC/sBTC (BTC Pool V2) { + address: '0xf32e91464ca18fc156ab97a697d6f8ae66cd21a3', swapAddress: '0xdf3309771d2bf82cb2b6c56f9f5365c8bd97c4f2', - tokenAddress: '0xf32e91464ca18fc156ab97a697d6f8ae66cd21a3', }, // Saddle DAI/USDC/USDT (v1) { + address: '0x76204f8cfe8b95191a3d1cfa59e267ea65e06fac', swapAddress: '0x3911f80530595fbd01ab1516ab61255d75aeb066', - tokenAddress: '0x76204f8cfe8b95191a3d1cfa59e267ea65e06fac', }, // Saddle WETH/vETH2 (v1) { + address: '0xe37e2a01fea778bc1717d72bd9f018b6a6b241d5', swapAddress: '0xdec2157831d6abc3ec328291119cc91b337272b5', - tokenAddress: '0xe37e2a01fea778bc1717d72bd9f018b6a6b241d5', }, // Saddle alETH (v2) { + address: '0xc9da65931abf0ed1b74ce5ad8c041c4220940368', swapAddress: '0xa6018520eaacc06c30ff2e1b3ee2c7c22e64196a', - tokenAddress: '0xc9da65931abf0ed1b74ce5ad8c041c4220940368', }, // Saddle D4 (v2) { + address: '0xd48cf4d7fb0824cc8bae055df3092584d0a1726a', swapAddress: '0xc69ddcd4dfef25d8a793241834d4cc4b3668ead6', - tokenAddress: '0xd48cf4d7fb0824cc8bae055df3092584d0a1726a', }, // Saddle 4Pool { + address: '0x1b4ab394327fdf9524632ddf2f0f04f9fa1fe2ec', swapAddress: '0x101cd330d088634b6f64c2eb4276e63bf1bbfde3', - tokenAddress: '0x1b4ab394327fdf9524632ddf2f0f04f9fa1fe2ec', }, // Saddle Frax 3Pool { + address: '0x0785addf5f7334adb7ec40cd785ebf39bfd91520', swapAddress: '0x8caea59f3bf1f341f89c51607e4919841131e47a', - tokenAddress: '0x0785addf5f7334adb7ec40cd785ebf39bfd91520', }, // Saddle tBTCV2/wBTC/renBTC/sBTC (tBTCv2 Metapool V1) { + address: '0x122eca07139eb368245a29fb702c9ff11e9693b7', swapAddress: '0xf74ebe6e5586275dc4ced78f5dbef31b1efbe7a5', - tokenAddress: '0x122eca07139eb368245a29fb702c9ff11e9693b7', }, // Saddle tBTCV2/wBTC/renBTC/sBTC (tBTCv2 Metapool V2) { + address: '0x3f2f811605bc6d701c3ad6e501be13461c560320', swapAddress: '0xa0b4a2667dd60d5cdd7ecff1084f0ceb8dd84326', - tokenAddress: '0x3f2f811605bc6d701c3ad6e501be13461c560320', }, ]; diff --git a/src/apps/saddle/ethereum/saddle.pool.token-fetcher.ts b/src/apps/saddle/ethereum/saddle.pool.token-fetcher.ts index 7b23aa0c3..9032d5965 100644 --- a/src/apps/saddle/ethereum/saddle.pool.token-fetcher.ts +++ b/src/apps/saddle/ethereum/saddle.pool.token-fetcher.ts @@ -1,58 +1,44 @@ import { Inject } from '@nestjs/common'; import { BigNumber } from 'ethers'; -import { Register } from '~app-toolkit/decorators'; +import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface'; +import { PositionTemplate } from '~app-toolkit/decorators/position-template.decorator'; import { - CurvePoolOnChainCoinStrategy, - CurvePoolOnChainReserveStrategy, - CurvePoolTokenHelper, - CurvePoolVirtualPriceStrategy, -} from '~apps/curve'; -import { PositionFetcher } from '~position/position-fetcher.interface'; -import { AppTokenPosition } from '~position/position.interface'; -import { Network } from '~types/network.interface'; + CurvePoolDefinition, + CurvePoolStaticTokenFetcher, + ResolvePoolCoinAddressParams, + ResolvePoolReserveParams, +} from '~apps/curve/common/curve.pool-static.token-fetcher'; import { SaddleContractFactory, SaddleSwap } from '../contracts'; -import { SADDLE_DEFINITION } from '../saddle.definition'; import { SADDLE_POOL_DEFINITIONS } from './saddle.pool.definitions'; -@Register.TokenPositionFetcher({ - appId: SADDLE_DEFINITION.id, - groupId: SADDLE_DEFINITION.groups.pool.id, - network: Network.ETHEREUM_MAINNET, -}) -export class EthereumSaddlePoolTokenFetcher implements PositionFetcher { +@PositionTemplate() +export class EthereumSaddlePoolTokenFetcher extends CurvePoolStaticTokenFetcher { + groupLabel = 'Pools'; + poolDefinitions = SADDLE_POOL_DEFINITIONS; + constructor( - @Inject(CurvePoolTokenHelper) - private readonly curvePoolTokenHelper: CurvePoolTokenHelper, - @Inject(CurvePoolOnChainCoinStrategy) - private readonly curvePoolOnChainCoinStrategy: CurvePoolOnChainCoinStrategy, - @Inject(CurvePoolOnChainReserveStrategy) - private readonly curvePoolOnChainReserveStrategy: CurvePoolOnChainReserveStrategy, - @Inject(CurvePoolVirtualPriceStrategy) - private readonly curvePoolVirtualPriceStrategy: CurvePoolVirtualPriceStrategy, - @Inject(SaddleContractFactory) - private readonly saddleContractFactory: SaddleContractFactory, - ) {} - - async getPositions() { - return this.curvePoolTokenHelper.getTokens({ - network: Network.ETHEREUM_MAINNET, - appId: SADDLE_DEFINITION.id, - groupId: SADDLE_DEFINITION.groups.pool.id, - poolDefinitions: SADDLE_POOL_DEFINITIONS, - resolvePoolContract: ({ network, address }) => this.saddleContractFactory.saddleSwap({ network, address }), - resolvePoolCoinAddresses: this.curvePoolOnChainCoinStrategy.build({ - resolveCoinAddress: ({ multicall, poolContract, index }) => multicall.wrap(poolContract).getToken(index), - }), - resolvePoolReserves: this.curvePoolOnChainReserveStrategy.build({ - resolveReserve: ({ multicall, poolContract, index }) => multicall.wrap(poolContract).getTokenBalance(index), - }), - resolvePoolTokenPrice: this.curvePoolVirtualPriceStrategy.build({ - resolveVirtualPrice: ({ multicall, poolContract }) => multicall.wrap(poolContract).getVirtualPrice(), - }), - resolvePoolFee: async () => BigNumber.from('4000000'), - }); + @Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit, + @Inject(SaddleContractFactory) protected readonly contractFactory: SaddleContractFactory, + ) { + super(appToolkit, contractFactory); + } + + resolvePoolContract(definition: CurvePoolDefinition): SaddleSwap { + return this.contractFactory.saddleSwap({ address: definition.swapAddress, network: this.network }); + } + + async resolvePoolCoinAddress({ contract, index }: ResolvePoolCoinAddressParams) { + return contract.getToken(index); + } + + async resolvePoolReserve({ contract, index }: ResolvePoolReserveParams) { + return contract.getTokenBalance(index); + } + + async resolvePoolFee() { + return BigNumber.from('4000000'); } } diff --git a/src/apps/saddle/saddle.module.ts b/src/apps/saddle/saddle.module.ts index 91ab6db1c..8ad8c8ff9 100644 --- a/src/apps/saddle/saddle.module.ts +++ b/src/apps/saddle/saddle.module.ts @@ -1,15 +1,7 @@ import { Register } from '~app-toolkit/decorators'; import { AbstractApp } from '~app/app.dynamic-module'; -import { - CurvePoolOnChainCoinStrategy, - CurvePoolOnChainReserveStrategy, - CurvePoolTokenHelper, - CurvePoolVirtualPriceStrategy, -} from '~apps/curve'; -import { SynthetixAppModule } from '~apps/synthetix'; import { SaddleContractFactory } from './contracts'; -import { EthereumSaddleBalanceFetcher } from './ethereum/saddle.balance-fetcher'; import { EthereumSaddleCommunalFarmContractPositionFetcher } from './ethereum/saddle.communal-farm.contract-position-fetcher'; import { EthereumSaddleMiniChefV2FarmContractPositionFetcher } from './ethereum/saddle.mini-chef-v2-farm.contract-position-fetcher'; import { EthereumSaddlePoolTokenFetcher } from './ethereum/saddle.pool.token-fetcher'; @@ -17,17 +9,10 @@ import { SaddleAppDefinition, SADDLE_DEFINITION } from './saddle.definition'; @Register.AppModule({ appId: SADDLE_DEFINITION.id, - imports: [SynthetixAppModule], providers: [ SaddleAppDefinition, SaddleContractFactory, - // Helpers - CurvePoolTokenHelper, - CurvePoolVirtualPriceStrategy, - CurvePoolOnChainCoinStrategy, - CurvePoolOnChainReserveStrategy, // Ethereum - EthereumSaddleBalanceFetcher, EthereumSaddleCommunalFarmContractPositionFetcher, EthereumSaddleMiniChefV2FarmContractPositionFetcher, EthereumSaddlePoolTokenFetcher, diff --git a/src/position/template/master-chef.template.contract-position-fetcher.ts b/src/position/template/master-chef.template.contract-position-fetcher.ts index 5e47d10f1..ea012cc85 100644 --- a/src/position/template/master-chef.template.contract-position-fetcher.ts +++ b/src/position/template/master-chef.template.contract-position-fetcher.ts @@ -3,6 +3,7 @@ import { BigNumber, BigNumberish, Contract } from 'ethers'; import { isArray, range, sum } from 'lodash'; import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface'; +import { ZERO_ADDRESS } from '~app-toolkit/constants/address'; import { BLOCKS_PER_DAY } from '~app-toolkit/constants/blocks'; import { RewardRateUnit } from '~app-toolkit/helpers/master-chef/master-chef.contract-position-helper'; import { getImagesFromToken, getLabelFromToken } from '~app-toolkit/helpers/presentation/image.present'; @@ -99,7 +100,7 @@ export abstract class MasterChefTemplateContractPositionFetcher< throw err; }); - if (!stakedTokenAddress || !rewardTokenAddresses) return null; + if (!stakedTokenAddress || stakedTokenAddress === ZERO_ADDRESS || !rewardTokenAddresses) return null; tokenDefinitions.push({ metaType: MetaType.SUPPLIED, address: stakedTokenAddress, network: this.network }); rewardTokenAddresses.forEach(v =>