diff --git a/src/apps/uniswap-v2/ethereum/uniswap-v2.balance-fetcher.ts b/src/apps/uniswap-v2/ethereum/uniswap-v2.balance-fetcher.ts deleted file mode 100644 index 29e72f30f..000000000 --- a/src/apps/uniswap-v2/ethereum/uniswap-v2.balance-fetcher.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Inject } from '@nestjs/common'; - -import { IAppToolkit, APP_TOOLKIT } 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 { Network } from '~types/network.interface'; - -import { UniswapV2TheGraphPoolTokenBalanceHelper } from '../helpers/uniswap-v2.the-graph.pool-token-balance-helper'; -import UNISWAP_V2_DEFINITION from '../uniswap-v2.definition'; - -const appId = UNISWAP_V2_DEFINITION.id; -const network = Network.ETHEREUM_MAINNET; - -@Register.BalanceFetcher(appId, network) -export class EthereumUniswapV2BalanceFetcher implements BalanceFetcher { - constructor( - @Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit, - @Inject(UniswapV2TheGraphPoolTokenBalanceHelper) - private readonly uniswapV2TheGraphPoolTokenBalanceHelper: UniswapV2TheGraphPoolTokenBalanceHelper, - ) {} - - private getPoolTokenBalances(address: string) { - return this.uniswapV2TheGraphPoolTokenBalanceHelper.getBalances({ - appId, - groupId: UNISWAP_V2_DEFINITION.groups.pool.id, - network, - address, - subgraphUrl: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2', - symbolPrefix: 'UNI-V2', - }); - } - - async getBalances(address: string) { - const balances = await this.getPoolTokenBalances(address); - - return presentBalanceFetcherResponse([ - { - label: 'Pools', - assets: balances, - }, - ]); - } -} diff --git a/src/apps/uniswap-v2/ethereum/uniswap-v2.pool.token-fetcher.ts b/src/apps/uniswap-v2/ethereum/uniswap-v2.pool.token-fetcher.ts index 0b1371d37..2c5bb266f 100644 --- a/src/apps/uniswap-v2/ethereum/uniswap-v2.pool.token-fetcher.ts +++ b/src/apps/uniswap-v2/ethereum/uniswap-v2.pool.token-fetcher.ts @@ -1,117 +1,101 @@ -import { Inject } from '@nestjs/common'; +import { gql } from 'graphql-request'; +import { chunk, compact } from 'lodash'; -import { Register } from '~app-toolkit/decorators'; -import { PositionFetcher } from '~position/position-fetcher.interface'; -import { AppTokenPosition } from '~position/position.interface'; -import { Network } from '~types/network.interface'; +import { drillBalance } from '~app-toolkit'; +import { ZERO_ADDRESS } from '~app-toolkit/constants/address'; +import { PositionTemplate } from '~app-toolkit/decorators/position-template.decorator'; +import { AppTokenPositionBalance, RawAppTokenBalance } from '~position/position-balance.interface'; +import { isAppToken } from '~position/position.interface'; -import { UniswapFactory, UniswapPair, UniswapV2ContractFactory } from '../contracts'; -import { UniswapV2OnChainTokenDerivationStrategy } from '../helpers/uniswap-v2.on-chain.token-derivation-strategy'; -import { UniswapV2PoolTokenHelper } from '../helpers/uniswap-v2.pool.token-helper'; -import { UniswapV2TheGraphPoolTokenAddressStrategy } from '../helpers/uniswap-v2.the-graph.pool-token-address-strategy'; -import { UniswapV2TheGraphPoolVolumeStrategy } from '../helpers/uniswap-v2.the-graph.pool-volume-strategy'; -import UNISWAP_V2_DEFINITION from '../uniswap-v2.definition'; +import { UniswapV2DefaultPoolSubgraphTemplateTokenFetcher } from '../common/uniswap-v2.default.subgraph.template.token-fetcher'; +import { UniswapV2TokenDataProps } from '../common/uniswap-v2.pool.on-chain.template.token-fetcher'; -const appId = UNISWAP_V2_DEFINITION.id; -const groupId = UNISWAP_V2_DEFINITION.groups.pool.id; -const network = Network.ETHEREUM_MAINNET; +type UniswapV2BalancesData = { + user?: { + liquidityPositions: { + pair: { + id: string; + }; + }[]; + }; +}; -@Register.TokenPositionFetcher({ appId, groupId, network }) -export class EthereumUniswapV2PoolTokenFetcher implements PositionFetcher { - constructor( - @Inject(UniswapV2ContractFactory) - private readonly uniswapV2ContractFactory: UniswapV2ContractFactory, - @Inject(UniswapV2PoolTokenHelper) - private readonly uniswapV2PoolTokenHelper: UniswapV2PoolTokenHelper, - @Inject(UniswapV2OnChainTokenDerivationStrategy) - private readonly uniswapV2OnChainTokenDerivationStrategy: UniswapV2OnChainTokenDerivationStrategy, - @Inject(UniswapV2TheGraphPoolTokenAddressStrategy) - private readonly uniswapV2TheGraphPoolTokenAddressStrategy: UniswapV2TheGraphPoolTokenAddressStrategy, - @Inject(UniswapV2TheGraphPoolVolumeStrategy) - private readonly uniswapV2TheGraphPoolVolumeStrategy: UniswapV2TheGraphPoolVolumeStrategy, - ) {} +const UNISWAP_V2_BALANCES_QUERY = gql` + query getBalances($address: String!) { + user(id: $address) { + liquidityPositions { + pair { + id + } + } + } + } +`; - async getPositions() { - return this.uniswapV2PoolTokenHelper.getTokens({ - network, - appId, - groupId: UNISWAP_V2_DEFINITION.groups.pool.id, - factoryAddress: '0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f', - hiddenTokens: [ - '0x62359ed7505efc61ff1d56fef82158ccaffa23d7', - '0x35bd01fc9d6d5d81ca9e055db88dc49aa2c699a8', - '0x6adb2e268de2aa1abf6578e4a8119b960e02928f', - ], - blockedPools: ['0x9cbfb60a09a9a33a10312da0f39977cbdb7fde23'], // Uniswap V2: SAITAMA - has a transfer fee (not supported by our zap) - resolveFactoryContract: ({ address, network }) => - this.uniswapV2ContractFactory.uniswapFactory({ address, network }), - resolvePoolContract: ({ address, network }) => this.uniswapV2ContractFactory.uniswapPair({ address, network }), - resolvePoolTokenAddresses: this.uniswapV2TheGraphPoolTokenAddressStrategy.build({ - subgraphUrl: 'https://api.thegraph.com/subgraphs/name/ianlapham/uniswapv2', - first: 1000, - orderBy: 'trackedReserveETH', - requiredPools: [ - // pairs that should be tracked but arent due to lag in subgraph - '0x9928e4046d7c6513326ccea028cd3e7a91c7590a', // FEI/TRIBE - '0xcd7989894bc033581532d2cd88da5db0a4b12859', // WBTC/BADGER - '0xe1573b9d29e2183b1af0e743dc2754979a40d237', // FXS/FRAX - '0x709f7b10f22eb62b05913b59b92ddd372d4e2152', // PAXG/USDP - // other pairs tracked for farms, etc. - '0xaad22f5543fcdaa694b68f94be177b561836ae57', // sUSD/$BASED - '0xe98f89a2b3aecdbe2118202826478eb02434459a', // DAI/DEBASE - '0x980a07e4f64d21a0cb2ef8d4af362a79b9f5c0da', // DAI/BSGS - '0xf58d2bacbc68c587730ea0ce5131f6ae7c622a5d', // ORCL5/ETH - '0xc3601f3e1c26d1a47571c559348e4156786d1fec', // DUCK/WETH - '0xcadd30b39f01cfdfb848174b19bbb5b1b7486159', // DSU/ESS - '0x0bf46ba06dc1d33c3bd80ff42497ebff13a88900', // rDPX/ETH - '0xfd0a40bc83c5fae4203dec7e5929b446b07d1c76', // FRAX/ETH - '0x97c4adc5d28a86f9470c70dd91dc6cc2f20d2d4d', // FRAX/USDC - '0x1f249f8b5a42aa78cc8a2b66ee0bb015468a5f43', // wETH/wBAN - ], - }), - resolveDerivedUnderlyingToken: this.uniswapV2OnChainTokenDerivationStrategy.build({ - priceDerivationWhitelist: [ - '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // WETH - '0x6b175474e89094c44da98b954eedeac495271d0f', // DAI - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC - '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT - '0x853d955acef822db058eb8505911ed77f175b99e', // FRAX - '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', // WBTC - ], - resolvePoolAddress: ({ factoryContract, multicall, token0, token1 }) => - multicall.wrap(factoryContract).getPair(token0, token1), - }), - resolvePoolTokenSymbol: ({ multicall, poolContract }) => multicall.wrap(poolContract).symbol(), - resolvePoolTokenSupply: ({ multicall, poolContract }) => multicall.wrap(poolContract).totalSupply(), - resolvePoolReserves: async ({ multicall, poolContract }) => - multicall - .wrap(poolContract) - .getReserves() - .then(v => [v[0], v[1]]), - resolvePoolUnderlyingTokenAddresses: async ({ multicall, poolContract }) => - Promise.all([multicall.wrap(poolContract).token0(), multicall.wrap(poolContract).token1()]), - resolveTokenDisplaySymbol: token => (token.symbol === 'WETH' ? 'ETH' : token.symbol), - resolvePoolVolumes: this.uniswapV2TheGraphPoolVolumeStrategy.build({ - subgraphUrl: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2', - first: 1000, - requiredPools: [ - // pairs that should be tracked but arent due to lag in subgraph - '0x9928e4046d7c6513326ccea028cd3e7a91c7590a', // FEI/TRIBE - '0xcd7989894bc033581532d2cd88da5db0a4b12859', // WBTC/BADGER - '0xe1573b9d29e2183b1af0e743dc2754979a40d237', // FXS/FRAX - '0x709f7b10f22eb62b05913b59b92ddd372d4e2152', // PAXG/USDP - // other pairs tracked for farms, etc. - '0xaad22f5543fcdaa694b68f94be177b561836ae57', // sUSD/$BASED - '0xe98f89a2b3aecdbe2118202826478eb02434459a', // DAI/DEBASE - '0x980a07e4f64d21a0cb2ef8d4af362a79b9f5c0da', // DAI/BSGS - '0xf58d2bacbc68c587730ea0ce5131f6ae7c622a5d', // ORCL5/ETH - '0xc3601f3e1c26d1a47571c559348e4156786d1fec', // DUCK/WETH - '0xcadd30b39f01cfdfb848174b19bbb5b1b7486159', // DSU/ESS - '0x0bf46ba06dc1d33c3bd80ff42497ebff13a88900', // rDPX/ETH - '0xfd0a40bc83c5fae4203dec7e5929b446b07d1c76', // FRAX/ETH - '0x97c4adc5d28a86f9470c70dd91dc6cc2f20d2d4d', // FRAX/USDC - ], +@PositionTemplate() +export class EthereumUniswapV2PoolTokenFetcher extends UniswapV2DefaultPoolSubgraphTemplateTokenFetcher { + groupLabel = 'Pools'; + + factoryAddress = '0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f'; + subgraphUrl = 'https://api.thegraph.com/subgraphs/name/ianlapham/uniswapv2'; + first = 5000; + + async getBalances(_address: string): Promise[]> { + const multicall = this.appToolkit.getMulticall(this.network); + const tokenLoader = this.appToolkit.getTokenDependencySelector(); + const address = await this.getAccountAddress(_address); + if (address === ZERO_ADDRESS) return []; + + // Use the subgraph to determine holdings. Later, we'll optimize this to use our own holdings by default. + const data = await this.appToolkit.helpers.theGraphHelper.requestGraph({ + endpoint: this.subgraphUrl, + query: UNISWAP_V2_BALANCES_QUERY, + variables: { address: address.toLowerCase() }, + }); + + const heldTokenAddresses = data.user?.liquidityPositions.map(v => v.pair.id.toLowerCase()) ?? []; + const heldTokens = await tokenLoader.getMany(heldTokenAddresses.map(t => ({ address: t, network: this.network }))); + const sanitized = compact(heldTokens).filter(isAppToken); + + const balances = await Promise.all( + sanitized.map(async appToken => { + const balanceRaw = await this.getBalancePerToken({ multicall, address, appToken }); + const tokenBalance = drillBalance(appToken, balanceRaw.toString(), { isDebt: this.isDebt }); + return tokenBalance; }), + ); + + return balances as AppTokenPositionBalance[]; + } + + async getRawBalances(_address: string): Promise { + const multicall = this.appToolkit.getMulticall(this.network); + const tokenLoader = this.appToolkit.getTokenDependencySelector(); + const address = await this.getAccountAddress(_address); + if (address === ZERO_ADDRESS) return []; + + // Use the subgraph to determine holdings. Later, we'll optimize this to use our own holdings by default. + const data = await this.appToolkit.helpers.theGraphHelper.requestGraph({ + endpoint: this.subgraphUrl, + query: UNISWAP_V2_BALANCES_QUERY, + variables: { address: address.toLowerCase() }, }); + + const heldTokenAddresses = data.user?.liquidityPositions.map(v => v.pair.id.toLowerCase()) ?? []; + const heldTokens = await tokenLoader.getMany(heldTokenAddresses.map(t => ({ address: t, network: this.network }))); + const sanitized = compact(heldTokens).filter(isAppToken); + + let results: RawAppTokenBalance[] = []; + for (const batch of chunk(sanitized, 100).values()) { + results = results.concat( + await Promise.all( + batch.map(async appToken => ({ + key: this.appToolkit.getPositionKey(appToken), + balance: (await this.getBalancePerToken({ multicall, address, appToken })).toString(), + })), + ), + ); + } + return results; } } diff --git a/src/apps/uniswap-v2/helpers/uniswap-v2.the-graph.tvl-helper.ts b/src/apps/uniswap-v2/helpers/uniswap-v2.the-graph.tvl-helper.ts deleted file mode 100644 index bdc06de73..000000000 --- a/src/apps/uniswap-v2/helpers/uniswap-v2.the-graph.tvl-helper.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { gql } from 'graphql-request'; - -import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface'; - -type UniswapV2TheGraphTvlHelperParams = { - subgraphUrl: string; - factoryObjectName?: string; - tvlObjectName?: string; -}; - -@Injectable() -export class UniswapV2TheGraphTvlHelper { - constructor(@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit) {} - - async getTvl({ - subgraphUrl, - factoryObjectName = 'uniswapFactories', - tvlObjectName = 'totalLiquidityUSD', - }: UniswapV2TheGraphTvlHelperParams) { - const query = gql` - query getTvl { - ${factoryObjectName} { - ${tvlObjectName} - } - } - `; - - const data = await this.appToolkit.helpers.theGraphHelper.request({ endpoint: subgraphUrl, query }); - const tvl = data[factoryObjectName]?.[0]?.[tvlObjectName] ?? 0; - return Number(tvl); - } -} diff --git a/src/apps/uniswap-v2/uniswap-v2.module.ts b/src/apps/uniswap-v2/uniswap-v2.module.ts index 9f6eb41bb..5b572ef37 100644 --- a/src/apps/uniswap-v2/uniswap-v2.module.ts +++ b/src/apps/uniswap-v2/uniswap-v2.module.ts @@ -2,7 +2,6 @@ import { Register } from '~app-toolkit/decorators'; import { AbstractApp } from '~app/app.dynamic-module'; import { UniswapV2ContractFactory } from './contracts'; -import { EthereumUniswapV2BalanceFetcher } from './ethereum/uniswap-v2.balance-fetcher'; import { EthereumUniswapV2PoolTokenFetcher } from './ethereum/uniswap-v2.pool.token-fetcher'; import { UniswapV2OnChainPoolTokenAddressStrategy } from './helpers/uniswap-v2.on-chain.pool-token-address-strategy'; import { UniswapV2OnChainTokenDerivationStrategy } from './helpers/uniswap-v2.on-chain.token-derivation-strategy'; @@ -10,7 +9,6 @@ import { UniswapV2PoolTokenHelper } from './helpers/uniswap-v2.pool.token-helper import { UniswapV2TheGraphPoolTokenAddressStrategy } from './helpers/uniswap-v2.the-graph.pool-token-address-strategy'; import { UniswapV2TheGraphPoolTokenBalanceHelper } from './helpers/uniswap-v2.the-graph.pool-token-balance-helper'; import { UniswapV2TheGraphPoolVolumeStrategy } from './helpers/uniswap-v2.the-graph.pool-volume-strategy'; -import { UniswapV2TheGraphTvlHelper } from './helpers/uniswap-v2.the-graph.tvl-helper'; import { UNISWAP_V2_DEFINITION, UniswapV2AppDefinition } from './uniswap-v2.definition'; @Register.AppModule({ @@ -19,7 +17,6 @@ import { UNISWAP_V2_DEFINITION, UniswapV2AppDefinition } from './uniswap-v2.defi UniswapV2AppDefinition, UniswapV2ContractFactory, EthereumUniswapV2PoolTokenFetcher, - EthereumUniswapV2BalanceFetcher, // Helpers UniswapV2PoolTokenHelper, UniswapV2OnChainPoolTokenAddressStrategy, @@ -27,7 +24,6 @@ import { UNISWAP_V2_DEFINITION, UniswapV2AppDefinition } from './uniswap-v2.defi UniswapV2TheGraphPoolTokenAddressStrategy, UniswapV2TheGraphPoolVolumeStrategy, UniswapV2TheGraphPoolTokenBalanceHelper, - UniswapV2TheGraphTvlHelper, ], exports: [ UniswapV2ContractFactory, @@ -37,7 +33,6 @@ import { UNISWAP_V2_DEFINITION, UniswapV2AppDefinition } from './uniswap-v2.defi UniswapV2TheGraphPoolTokenAddressStrategy, UniswapV2TheGraphPoolVolumeStrategy, UniswapV2TheGraphPoolTokenBalanceHelper, - UniswapV2TheGraphTvlHelper, ], }) export class UniswapV2AppModule extends AbstractApp() {}