diff --git a/modules/balancer/balancer.gql b/modules/balancer/balancer.gql deleted file mode 100644 index 228429a2f..000000000 --- a/modules/balancer/balancer.gql +++ /dev/null @@ -1,39 +0,0 @@ -extend type Mutation { - balancerMutationTest: String! -} - -extend type Query { - sorGetCowSwaps( - chain: GqlChain! - tokenIn: String! - tokenOut: String! - swapType: GqlSorSwapType! - swapAmount: BigDecimal! #expected in raw amount - ): GqlCowSwapApiResponse! -} - -enum GqlSorSwapType { - EXACT_IN - EXACT_OUT -} - -type GqlCowSwapApiResponse { - tokenAddresses: [String!]! - swaps: [GqlSwap!]! - swapAmount: String! - swapAmountForSwaps: String! - returnAmount: String! - returnAmountFromSwaps: String! - returnAmountConsideringFees: String! - tokenIn: String! - tokenOut: String! - marketSp: String! -} - -type GqlSwap { - poolId: String! - assetInIndex: Int! - assetOutIndex: Int! - amount: String! - userData: String! -} diff --git a/modules/balancer/balancer.resolvers.ts b/modules/balancer/balancer.resolvers.ts deleted file mode 100644 index 3369aa1e0..000000000 --- a/modules/balancer/balancer.resolvers.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Resolvers } from '../../schema'; -import { sorService } from '../sor/sor.service'; -import { getTokenAmountRaw } from '../sor/utils'; - -const balancerResolvers: Resolvers = { - Query: { - sorGetCowSwaps: async (parent, args, context) => { - const amountToken = args.swapType === 'EXACT_IN' ? args.tokenIn : args.tokenOut; - // Use TokenAmount to help follow scaling requirements in later logic - // args.swapAmount is RawScale, e.g. 1USDC should be passed as 1000000 - const amount = await getTokenAmountRaw(amountToken, args.swapAmount, args.chain); - const swaps = await sorService.getCowSwaps({ ...args, swapAmount: amount, swapOptions: {} }); - return { ...swaps, __typename: 'GqlCowSwapApiResponse' }; - }, - }, - Mutation: { - balancerMutationTest: async (parent, {}, context) => { - return 'test'; - }, - }, -}; - -export default balancerResolvers; diff --git a/modules/beethoven/balancer-sdk.resolvers.ts b/modules/beethoven/balancer-sdk.resolvers.ts deleted file mode 100644 index a755a1e90..000000000 --- a/modules/beethoven/balancer-sdk.resolvers.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Resolvers } from '../../schema'; -import { balancerSorService } from './balancer-sor.service'; -import { tokenService } from '../token/token.service'; -import { sorService } from '../sor/sor.service'; -import { getTokenAmountHuman } from '../sor/utils'; -import { headerChain } from '../context/header-chain'; - -const balancerSdkResolvers: Resolvers = { - Query: { - sorGetSwaps: async (parent, args, context) => { - console.log('sorGetSwaps args', JSON.stringify(args)); - - const currentChain = headerChain(); - if (!args.chain && currentChain) { - args.chain = currentChain; - } else if (!args.chain) { - throw new Error('sorGetSwaps error: Provide "chain" param'); - } - const chain = args.chain; - const tokenIn = args.tokenIn.toLowerCase(); - const tokenOut = args.tokenOut.toLowerCase(); - const amountToken = args.swapType === 'EXACT_IN' ? tokenIn : tokenOut; - // Use TokenAmount to help follow scaling requirements in later logic - // args.swapAmount is HumanScale - const amount = await getTokenAmountHuman(amountToken, args.swapAmount, args.chain); - - const swaps = await sorService.getBeetsSwaps({ - ...args, - chain, - tokenIn, - tokenOut, - swapAmount: amount, - }); - - return { ...swaps, __typename: 'GqlSorGetSwapsResponse' }; - }, - sorGetBatchSwapForTokensIn: async (parent, args, context) => { - const tokens = await tokenService.getTokens(); - - return balancerSorService.getBatchSwapForTokensIn({ ...args, tokens }); - }, - }, -}; - -export default balancerSdkResolvers; diff --git a/modules/network/arbitrum.ts b/modules/network/arbitrum.ts index 3a34524b7..9b889c240 100644 --- a/modules/network/arbitrum.ts +++ b/modules/network/arbitrum.ts @@ -72,18 +72,6 @@ const arbitrumNetworkData: NetworkData = { }, balancer: { vault: '0xba12222222228d8ba445958a75a0704d566bf2c8', - composableStablePoolFactories: [ - '0xaeb406b0e430bf5ea2dc0b9fe62e4e53f74b3a33', - '0x85a80afee867adf27b50bdb7b76da70f1e853062', - '0x1c99324edc771c82a0dccb780cc7dda0045e50e7', - '0x2498a2b0d6462d2260eac50ae1c3e03f4829ba95', - '0xa8920455934da4d853faac1f94fe7bef72943ef1', - ], - weightedPoolV2Factories: [ - '0x8df6efec5547e31b0eb7d1291b511ff8a2bf987c', - '0xf1665e19bc105be4edd3739f88315cc699cc5b65', - '0xc7e5ed1054a24ef31d827e6f86caa58b3bc168d7', - ], swapProtocolFeePercentage: 0.5, yieldProtocolFeePercentage: 0.5, }, @@ -354,7 +342,7 @@ export const arbitrumNetworkConfig: NetworkConfig = { }, { name: 'feed-data-to-datastudio', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(1, 'minutes'), + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(5, 'minutes'), }, ], }; diff --git a/modules/network/avalanche.ts b/modules/network/avalanche.ts index 038f2788d..d633625d5 100644 --- a/modules/network/avalanche.ts +++ b/modules/network/avalanche.ts @@ -70,14 +70,6 @@ const avalancheNetworkData: NetworkData = { }, balancer: { vault: '0xba12222222228d8ba445958a75a0704d566bf2c8', - composableStablePoolFactories: [ - '0x3b1eb8eb7b43882b385ab30533d9a2bef9052a98', - '0xe42ffa682a26ef8f25891db4882932711d42e467', - ], - weightedPoolV2Factories: [ - '0x94f68b54191f62f781fe8298a8a5fa3ed772d227', - '0x230a59f4d9adc147480f03b0d3fffecd56c3289a', - ], swapProtocolFeePercentage: 0.5, yieldProtocolFeePercentage: 0.5, }, @@ -344,7 +336,11 @@ export const avalancheNetworkConfig: NetworkConfig = { }, { name: 'feed-data-to-datastudio', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(1, 'minutes'), + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(5, 'minutes'), + }, + { + name: 'sync-latest-fx-prices', + interval: every(10, 'minutes'), }, ], }; diff --git a/modules/network/base.ts b/modules/network/base.ts index 4509e5b4d..bd45b9005 100644 --- a/modules/network/base.ts +++ b/modules/network/base.ts @@ -70,8 +70,6 @@ const baseNetworkData: NetworkData = { }, balancer: { vault: '0xba12222222228d8ba445958a75a0704d566bf2c8', - composableStablePoolFactories: ['0x8df317a729fcaa260306d7de28888932cb579b88'], - weightedPoolV2Factories: ['0x4c32a8a8fda4e24139b51b456b42290f51d6a1c4'], swapProtocolFeePercentage: 0.5, yieldProtocolFeePercentage: 0.5, }, @@ -258,7 +256,7 @@ export const baseNetworkConfig: NetworkConfig = { }, { name: 'feed-data-to-datastudio', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(1, 'minutes'), + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(5, 'minutes'), }, ], }; diff --git a/modules/network/fantom.ts b/modules/network/fantom.ts index 9d800ae38..fbae4a3e0 100644 --- a/modules/network/fantom.ts +++ b/modules/network/fantom.ts @@ -39,7 +39,7 @@ const fantomNetworkData: NetworkData = { }, subgraphs: { startDate: '2021-10-08', - balancer: 'https://api.thegraph.com/subgraphs/name/beethovenxfi/beethovenx', + balancer: 'https://api.thegraph.com/subgraphs/name/beethovenxfi/beethovenx-v2-fantom', beetsBar: 'https://api.thegraph.com/subgraphs/name/beethovenxfi/beets-bar', blocks: 'https://api.thegraph.com/subgraphs/name/beethovenxfi/fantom-blocks', masterchef: 'https://api.thegraph.com/subgraphs/name/beethovenxfi/masterchefv2', @@ -94,8 +94,9 @@ const fantomNetworkData: NetworkData = { tokenPrices: { maxHourlyPriceHistoryNumDays: 100, }, - rpcUrl: - (env.DEPLOYMENT_ENV as DeploymentEnv) === 'main' ? `https://rpc.ankr.com/fantom` : `https://rpc.fantom.network`, + rpcUrl: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'main' + ? `https://rpc.ankr.com/fantom` + : `https://rpc.fantom.gateway.fm`, rpcMaxBlockRange: 1000, sanity: { projectId: '1g2ag2hb', @@ -114,27 +115,8 @@ const fantomNetworkData: NetworkData = { }, balancer: { vault: '0x20dd72ed959b6147912c2e529f0a0c651c33c9ce', - composableStablePoolFactories: [ - '0x5adaf6509bcec3219455348ac45d6d3261b1a990', - '0xb384a86f2fd7788720db42f9daa60fc07ecbea06', - '0x44814e3a603bb7f1198617995c5696c232f6e8ed', - '0x911566c808bf00acb200b418564440a2af177548', - '0x5c3094982cf3c97a06b7d62a6f7669f14a199b19', - '0x23f03a4fb344d8b98833d2ace093cc305e03474f', - ], - weightedPoolV2Factories: [ - '0xb2ed595afc445b47db7043bec25e772bf0fa1fbb', - '0x8ea1c497c16726e097f62c8c9fbd944143f27090', - '0xea87f3dffc679035653c0fba70e7bfe46e3fb733', - '0xd678b6acd834cc969bb19ce82727f2a541fb7941', - '0xb841df73861e65e6d61a80f503f095a91ce75e15', - ], swapProtocolFeePercentage: 0.25, yieldProtocolFeePercentage: 0.25, - factoriesWithpoolSpecificProtocolFeePercentagesProvider: [ - '0xb841df73861e65e6d61a80f503f095a91ce75e15', - '0x5c3094982cf3c97a06b7d62a6f7669f14a199b19', - ], }, multicall: '0x66335d7ad8011f6aa3f48aadcb523b62b38ed961', multicall3: '0xca11bde05977b3631167028862be2a173976ca11', @@ -460,7 +442,7 @@ export const fantomNetworkConfig: NetworkConfig = { }, { name: 'feed-data-to-datastudio', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(1, 'minutes'), + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(5, 'minutes'), }, ], }; diff --git a/modules/network/gnosis.ts b/modules/network/gnosis.ts index 360e227c7..728bf5a15 100644 --- a/modules/network/gnosis.ts +++ b/modules/network/gnosis.ts @@ -68,17 +68,6 @@ const gnosisNetworkData: NetworkData = { }, balancer: { vault: '0xba12222222228d8ba445958a75a0704d566bf2c8', - composableStablePoolFactories: [ - '0x76578ecf9a141296ec657847fb45b0585bcda3a6', - '0xc128468b7ce63ea702c1f104d55a2566b13d3abd', - '0xd87f44df0159dc78029ab9ca7d7e57e7249f5acd', - '0x4bdcc2fb18aeb9e2d281b0278d946445070eada7', - ], - weightedPoolV2Factories: [ - '0x6cad2ea22bfa7f4c14aae92e47f510cd5c509bc7', - '0xf302f9f50958c5593770fdf4d4812309ff77414f', - '0xc128a9954e6c874ea3d62ce62b468ba073093f25', - ], swapProtocolFeePercentage: 0.5, yieldProtocolFeePercentage: 0.5, }, @@ -271,7 +260,7 @@ export const gnosisNetworkConfig: NetworkConfig = { }, { name: 'feed-data-to-datastudio', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(1, 'minutes'), + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(5, 'minutes'), }, ], }; diff --git a/modules/network/mainnet.ts b/modules/network/mainnet.ts index fe2fd1676..016a27a21 100644 --- a/modules/network/mainnet.ts +++ b/modules/network/mainnet.ts @@ -83,22 +83,8 @@ const data: NetworkData = { balancer: { vault: '0xba12222222228d8ba445958a75a0704d566bf2c8', tokenAdmin: '0xf302f9f50958c5593770fdf4d4812309ff77414f', - composableStablePoolFactories: [ - '0xf9ac7b9df2b3454e841110cce5550bd5ac6f875f', - '0x85a80afee867adf27b50bdb7b76da70f1e853062', - '0xdba127fbc23fb20f5929c546af220a991b5c6e01', - '0xfada0f4547ab2de89d1304a668c39b3e09aa7c76', - '0xdb8d758bcb971e482b2c45f7f8a7740283a1bd3a', - '0xba1b4a90bad57470a2cba762a32955dc491f76e0', - ], - weightedPoolV2Factories: [ - '0xcc508a455f5b0073973107db6a878ddbdab957bc', - '0x5dd94da3644ddd055fcf6b3e1aa310bb7801eb8b', - '0x897888115ada5773e02aa29f775430bfb5f34c51', - ], swapProtocolFeePercentage: 0.5, yieldProtocolFeePercentage: 0.5, - excludedPoolDataQueryPoolIds: ['0xf71d0774b214c4cf51e33eb3d30ef98132e4dbaa00000000000000000000046e'], }, multicall: '0x5ba1e12693dc8f9c48aad8770482f4739beed696', multicall3: '0xca11bde05977b3631167028862be2a173976ca11', @@ -507,7 +493,11 @@ export const mainnetNetworkConfig: NetworkConfig = { }, { name: 'feed-data-to-datastudio', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(1, 'minutes'), + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(5, 'minutes'), + }, + { + name: 'sync-latest-fx-prices', + interval: every(10, 'minutes'), }, ], }; diff --git a/modules/network/network-config-types.ts b/modules/network/network-config-types.ts index 71a566434..da2886395 100644 --- a/modules/network/network-config-types.ts +++ b/modules/network/network-config-types.ts @@ -104,12 +104,8 @@ export interface NetworkData { balancer: { vault: string; tokenAdmin?: string; - weightedPoolV2Factories: string[]; - composableStablePoolFactories: string[]; yieldProtocolFeePercentage: number; swapProtocolFeePercentage: number; - excludedPoolDataQueryPoolIds?: string[]; - factoriesWithpoolSpecificProtocolFeePercentagesProvider?: string[]; }; multicall: string; multicall3: string; diff --git a/modules/network/optimism.ts b/modules/network/optimism.ts index c0f3beccb..ffe2b7a5c 100644 --- a/modules/network/optimism.ts +++ b/modules/network/optimism.ts @@ -81,17 +81,6 @@ const optimismNetworkData: NetworkData = { }, balancer: { vault: '0xba12222222228d8ba445958a75a0704d566bf2c8', - composableStablePoolFactories: [ - '0xf145cafb67081895ee80eb7c04a30cf87f07b745', - '0xe2e901ab09f37884ba31622df3ca7fc19aa443be', - '0x1802953277fd955f9a254b80aa0582f193cf1d77', - '0x043a2dad730d585c44fb79d2614f295d2d625412', - ], - weightedPoolV2Factories: [ - '0xad901309d9e9dbc5df19c84f729f429f0189a633', - '0xa0dabebaad1b243bbb243f933013d560819eb66f', - '0x230a59f4d9adc147480f03b0d3fffecd56c3289a', - ], swapProtocolFeePercentage: 0.5, yieldProtocolFeePercentage: 0.5, }, @@ -298,7 +287,10 @@ export const optimismNetworkConfig: NetworkConfig = { ], userStakedBalanceServices: [new UserSyncGaugeBalanceService()], services: { - balancerSubgraphService: new BalancerSubgraphService(optimismNetworkData.subgraphs.balancer, optimismNetworkData.chain.id), + balancerSubgraphService: new BalancerSubgraphService( + optimismNetworkData.subgraphs.balancer, + optimismNetworkData.chain.id, + ), }, /* For sub-minute jobs we set the alarmEvaluationPeriod and alarmDatapointsToAlarm to 1 instead of the default 3. @@ -404,7 +396,7 @@ export const optimismNetworkConfig: NetworkConfig = { }, { name: 'feed-data-to-datastudio', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(1, 'minutes'), + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(5, 'minutes'), }, ], }; diff --git a/modules/network/polygon.ts b/modules/network/polygon.ts index 2420ce629..398985199 100644 --- a/modules/network/polygon.ts +++ b/modules/network/polygon.ts @@ -72,18 +72,6 @@ const polygonNetworkData: NetworkData = { }, balancer: { vault: '0xba12222222228d8ba445958a75a0704d566bf2c8', - composableStablePoolFactories: [ - '0x136fd06fa01ecf624c7f2b3cb15742c1339dc2c4', - '0x85a80afee867adf27b50bdb7b76da70f1e853062', - '0x7bc6c0e73edaa66ef3f6e2f27b0ee8661834c6c9', - '0x6ab5549bbd766a43afb687776ad8466f8b42f777', - '0xe2fa4e1d17725e72dcdafe943ecf45df4b9e285b', - ], - weightedPoolV2Factories: [ - '0x0e39c3d9b2ec765efd9c5c70bb290b1fcd8536e3', - '0x82e4cfaef85b1b6299935340c964c942280327f4', - '0xfc8a407bba312ac761d8bfe04ce1201904842b76', - ], swapProtocolFeePercentage: 0.5, yieldProtocolFeePercentage: 0.5, }, @@ -389,7 +377,11 @@ export const polygonNetworkConfig: NetworkConfig = { }, { name: 'feed-data-to-datastudio', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(1, 'minutes'), + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(5, 'minutes'), + }, + { + name: 'sync-latest-fx-prices', + interval: every(10, 'minutes'), }, ], }; diff --git a/modules/network/zkevm.ts b/modules/network/zkevm.ts index 68a343a99..09a329daf 100644 --- a/modules/network/zkevm.ts +++ b/modules/network/zkevm.ts @@ -70,12 +70,6 @@ const zkevmNetworkData: NetworkData = { }, balancer: { vault: '0xba12222222228d8ba445958a75a0704d566bf2c8', - composableStablePoolFactories: [ - '0x8ea89804145c007e7d226001a96955ad53836087', - '0x956ccab09898c0af2aca5e6c229c3ad4e93d9288', - '0x577e5993b9cc480f07f98b5ebd055604bd9071c4', - ], - weightedPoolV2Factories: ['0x03f3fb107e74f2eac9358862e91ad3c692712054'], swapProtocolFeePercentage: 0.5, yieldProtocolFeePercentage: 0.5, }, @@ -290,7 +284,7 @@ export const zkevmNetworkConfig: NetworkConfig = { }, { name: 'feed-data-to-datastudio', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(1, 'minutes'), + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(5, 'minutes'), }, ], }; diff --git a/modules/pool/lib/pool-creator.service.ts b/modules/pool/lib/pool-creator.service.ts index 9235162e1..3191eed58 100644 --- a/modules/pool/lib/pool-creator.service.ts +++ b/modules/pool/lib/pool-creator.service.ts @@ -125,7 +125,9 @@ export class PoolCreatorService { const nestedPool = subgraphPools.find((nestedPool) => { const poolType = this.mapSubgraphPoolTypeToPoolType(nestedPool.poolType || ''); - return nestedPool.address === token.address && (poolType === 'LINEAR' || poolType === 'PHANTOM_STABLE'); + return ( + nestedPool.address === token.address && (poolType === 'LINEAR' || poolType === 'COMPOSABLE_STABLE') + ); }); if (nestedPool) { @@ -197,7 +199,7 @@ export class PoolCreatorService { const allNestedTypePools = await prisma.prismaPool.findMany({ where: { chain: this.chain, - type: { in: [PrismaPoolType.LINEAR, PrismaPoolType.PHANTOM_STABLE] }, + type: { in: [PrismaPoolType.LINEAR, PrismaPoolType.COMPOSABLE_STABLE] }, }, select: { id: true, address: true }, }); @@ -222,6 +224,12 @@ export class PoolCreatorService { ], }); + // for the old phantom stable pool, we add it to the DB as type COMPOSABLE_STABLE with version 0 + let poolTypeVersion = pool.poolTypeVersion ? pool.poolTypeVersion : 1; + if (pool.poolType === 'StablePhantom') { + poolTypeVersion = 0; + } + await prisma.prismaPool.create({ data: { id: pool.id, @@ -232,7 +240,7 @@ export class PoolCreatorService { name: pool.name || '', decimals: 18, type: poolType, - version: pool.poolTypeVersion ? pool.poolTypeVersion : 1, + version: poolTypeVersion, owner: pool.owner || ZERO_ADDRESS, factory: pool.factory, tokens: { @@ -308,7 +316,7 @@ export class PoolCreatorService { } : undefined, stableDynamicData: - poolType === 'STABLE' || poolType === 'PHANTOM_STABLE' || poolType === 'META_STABLE' + poolType === 'STABLE' || poolType === 'COMPOSABLE_STABLE' || poolType === 'META_STABLE' ? { create: { id: pool.id, @@ -410,15 +418,51 @@ export class PoolCreatorService { } } + public async updatePoolTypesAndVersionForAllPools() { + const subgraphPools = await this.balancerSubgraphService.getAllPools({}, false); + + for (const subgraphPool of subgraphPools) { + // for the old phantom stable pool, we add it to the DB as type COMPOSABLE_STABLE with version 0 + let poolTypeVersion = subgraphPool.poolTypeVersion ? subgraphPool.poolTypeVersion : 1; + if (subgraphPool.poolType === 'StablePhantom') { + poolTypeVersion = 0; + } + + const poolType = this.mapSubgraphPoolTypeToPoolType(subgraphPool.poolType || ''); + + try { + await prisma.prismaPool.update({ + where: { id_chain: { chain: networkContext.chain, id: subgraphPool.id } }, + data: { + version: poolTypeVersion, + type: poolType, + }, + }); + } catch (e: any) { + // Some pools are filtered from the DB, like test pools, + // so we just ignore them without breaking the loop + const error = e.meta ? e.meta.cause : e; + console.error( + 'Error in updating pool versions: ', + error, + 'Network', + networkContext.chain, + 'Pool ID: ', + subgraphPool.id, + ); + } + } + } + private sortSubgraphPools(subgraphPools: BalancerPoolFragment[]) { return _.sortBy(subgraphPools, (pool) => { const poolType = this.mapSubgraphPoolTypeToPoolType(pool.poolType || ''); if (poolType === 'LINEAR') { return 0; - } else if (poolType === 'PHANTOM_STABLE') { - //if the phantom stable has a nested phantom stable, it needs to appear later in the list - const nestedPhantomStableToken = (pool.tokens || []).find((token) => { + } else if (poolType === 'COMPOSABLE_STABLE') { + //if the composable stable has a nested composable stable, it needs to appear later in the list + const nestedComposableStableToken = (pool.tokens || []).find((token) => { if (token.address === pool.address) { return false; } @@ -426,10 +470,10 @@ export class PoolCreatorService { const nestedPool = subgraphPools.find((nestedPool) => nestedPool.address === token.address); const nestedPoolType = this.mapSubgraphPoolTypeToPoolType(nestedPool?.poolType || ''); - return nestedPoolType === 'PHANTOM_STABLE'; + return nestedPoolType === 'COMPOSABLE_STABLE'; }); - return nestedPhantomStableToken ? 2 : 1; + return nestedComposableStableToken ? 2 : 1; } return 3; @@ -446,10 +490,11 @@ export class PoolCreatorService { return 'STABLE'; case 'MetaStable': return 'META_STABLE'; + // for the old phantom stable pool, we add it to the DB as type COMPOSABLE_STABLE with version 0 case 'StablePhantom': - return 'PHANTOM_STABLE'; + return 'COMPOSABLE_STABLE'; case 'ComposableStable': - return 'PHANTOM_STABLE'; + return 'COMPOSABLE_STABLE'; case 'Linear': return 'LINEAR'; case 'Element': diff --git a/modules/pool/lib/pool-gql-loader.service.ts b/modules/pool/lib/pool-gql-loader.service.ts index 0f1be6426..6c6cdcff9 100644 --- a/modules/pool/lib/pool-gql-loader.service.ts +++ b/modules/pool/lib/pool-gql-loader.service.ts @@ -20,7 +20,7 @@ import { GqlPoolLinearNested, GqlPoolMinimal, GqlPoolNestingType, - GqlPoolPhantomStableNested, + GqlPoolComposableStableNested, GqlPoolStaking, GqlPoolToken, GqlPoolTokenDisplay, @@ -42,14 +42,16 @@ import { networkContext } from '../../network/network-context.service'; import { fixedNumber } from '../../view-helpers/fixed-number'; import { parseUnits } from 'ethers/lib/utils'; import { formatFixed } from '@ethersproject/bignumber'; -import { StringDecoder } from 'string_decoder'; -import { SwapKind } from '@balancer/sdk'; export class PoolGqlLoaderService { - public async getPool(id: string, chain: Chain): Promise { - const pool = await prisma.prismaPool.findUnique({ + public async getPool(id: string, chain: Chain, userAddress?: string): Promise { + let pool = undefined; + pool = await prisma.prismaPool.findUnique({ where: { id_chain: { id, chain: chain } }, - include: prismaPoolWithExpandedNesting.include, + include: { + ...prismaPoolWithExpandedNesting.include, + ...this.getUserBalancesInclude(userAddress), + }, }); if (!pool) { @@ -60,7 +62,7 @@ export class PoolGqlLoaderService { throw new Error('Pool exists, but has an unknown type'); } - return this.mapPoolToGqlPool(pool); + return this.mapPoolToGqlPool(pool, pool.userWalletBalances, pool.userStakedBalances); } public async getPools(args: QueryPoolGetPoolsArgs): Promise { @@ -79,24 +81,7 @@ export class PoolGqlLoaderService { ...this.mapQueryArgsToPoolQuery(args), include: { ...prismaPoolMinimal.include, - userWalletBalances: { - where: { - userAddress: { - equals: args.where?.userAddress, - mode: 'insensitive' as const, - }, - balanceNum: { gt: 0 }, - }, - }, - userStakedBalances: { - where: { - userAddress: { - equals: args.where?.userAddress, - mode: 'insensitive' as const, - }, - balanceNum: { gt: 0 }, - }, - }, + ...this.getUserBalancesInclude(args.where.userAddress), }, }); @@ -423,7 +408,11 @@ export class PoolGqlLoaderService { }; } - private mapPoolToGqlPool(pool: PrismaPoolWithExpandedNesting): GqlPoolUnion { + private mapPoolToGqlPool( + pool: PrismaPoolWithExpandedNesting, + userWalletbalances: PrismaUserWalletBalance[] = [], + userStakedBalances: PrismaUserStakedBalance[] = [], + ): GqlPoolUnion { const bpt = pool.tokens.find((token) => token.address === pool.address); const mappedData = { @@ -437,6 +426,7 @@ export class PoolGqlLoaderService { tokens: pool.tokens.map((token) => this.mapPoolTokenToGqlUnion(token)), allTokens: this.mapAllTokens(pool), displayTokens: this.mapDisplayTokens(pool), + userBalance: this.getUserBalance(pool, userWalletbalances, userStakedBalances), }; //TODO: may need to build out the types here still @@ -455,9 +445,9 @@ export class PoolGqlLoaderService { amp: pool.stableDynamicData?.amp || '0', tokens: mappedData.tokens as GqlPoolToken[], }; - case 'PHANTOM_STABLE': + case 'COMPOSABLE_STABLE': return { - __typename: 'GqlPoolPhantomStable', + __typename: 'GqlPoolComposableStable', ...mappedData, amp: pool.stableDynamicData?.amp || '0', bptPriceRate: bpt?.dynamicData?.priceRate || '1.0', @@ -608,7 +598,7 @@ export class PoolGqlLoaderService { weight: poolToken?.dynamicData?.weight, }; } - } else if (allToken.nestedPool?.type === 'PHANTOM_STABLE') { + } else if (allToken.nestedPool?.type === 'COMPOSABLE_STABLE') { const mainTokens = allToken.nestedPool.allTokens.filter( (nestedToken) => @@ -951,7 +941,7 @@ export class PoolGqlLoaderService { private getPoolInvestConfig(pool: PrismaPoolWithExpandedNesting): GqlPoolInvestConfig { const poolTokens = pool.tokens.filter((token) => token.address !== pool.address); - const supportsNativeAssetDeposit = pool.type !== 'PHANTOM_STABLE'; + const supportsNativeAssetDeposit = pool.type !== 'COMPOSABLE_STABLE'; let options: GqlPoolInvestOption[] = []; for (const poolToken of poolTokens) { @@ -960,7 +950,7 @@ export class PoolGqlLoaderService { return { //TODO could flag these as disabled in sanity - proportionalEnabled: pool.type !== 'PHANTOM_STABLE' && pool.type !== 'META_STABLE', + proportionalEnabled: pool.type !== 'COMPOSABLE_STABLE' && pool.type !== 'META_STABLE', singleAssetEnabled: true, options, }; @@ -1016,11 +1006,11 @@ export class PoolGqlLoaderService { ] : [this.mapPoolTokenToGql(mainToken)], }); - } else if (nestedPool && nestedPool.type === 'PHANTOM_STABLE') { + } else if (nestedPool && nestedPool.type === 'COMPOSABLE_STABLE') { const nestedTokens = nestedPool.tokens.filter((token) => token.address !== nestedPool.address); - if (pool.type === 'PHANTOM_STABLE' || isWeightedPoolV2(pool)) { - //when nesting a phantom stable inside a phantom stable, all of the underlying tokens can be used when investing + if (pool.type === 'COMPOSABLE_STABLE' || isWeightedPoolV2(pool)) { + //when nesting a composable stable inside a composable stable, all of the underlying tokens can be used when investing //when withdrawing from a v2 weighted pool, we withdraw into all underlying assets. // ie: USDC/DAI/USDT for nested bbaUSD for (const nestedToken of nestedTokens) { @@ -1040,7 +1030,7 @@ export class PoolGqlLoaderService { }); } } else { - //if the parent pool does not have phantom bpt (ie: weighted), the user can only invest with 1 of the phantom stable tokens + //if the parent pool does not have phantom bpt (ie: weighted), the user can only invest with 1 of the composable stable tokens options.push({ poolTokenIndex: poolToken.index, poolTokenAddress: poolToken.address, @@ -1101,7 +1091,7 @@ export class PoolGqlLoaderService { ...this.getLinearPoolTokenData(token, nestedPool), pool: this.mapNestedPoolToGqlPoolLinearNested(nestedPool, percentOfSupplyNested), }; - } else if (nestedPool && nestedPool.type === 'PHANTOM_STABLE') { + } else if (nestedPool && nestedPool.type === 'COMPOSABLE_STABLE') { const totalShares = parseFloat(nestedPool.dynamicData?.totalShares || '0'); const percentOfSupplyNested = totalShares > 0 ? parseFloat(token.dynamicData?.balance || '0') / totalShares : 0; @@ -1109,8 +1099,8 @@ export class PoolGqlLoaderService { //50_000_000_000_000 return { ...this.mapPoolTokenToGql(token), - __typename: 'GqlPoolTokenPhantomStable', - pool: this.mapNestedPoolToGqlPoolPhantomStableNested(nestedPool, percentOfSupplyNested), + __typename: 'GqlPoolTokenComposableStable', + pool: this.mapNestedPoolToGqlPoolComposableStableNested(nestedPool, percentOfSupplyNested), }; } @@ -1164,14 +1154,14 @@ export class PoolGqlLoaderService { }; } - private mapNestedPoolToGqlPoolPhantomStableNested( + private mapNestedPoolToGqlPoolComposableStableNested( pool: PrismaNestedPoolWithSingleLayerNesting, percentOfSupplyNested: number, - ): GqlPoolPhantomStableNested { + ): GqlPoolComposableStableNested { const bpt = pool.tokens.find((token) => token.address === pool.address); return { - __typename: 'GqlPoolPhantomStableNested', + __typename: 'GqlPoolComposableStableNested', ...pool, nestingType: this.getPoolNestingType(pool), tokens: pool.tokens.map((token) => { @@ -1258,4 +1248,30 @@ export class PoolGqlLoaderService { .toFixed(mainToken.token.decimals)}`, }; } + + private getUserBalancesInclude(userAddress?: string) { + if (!userAddress) { + return {}; + } + return { + userWalletBalances: { + where: { + userAddress: { + equals: userAddress, + mode: 'insensitive' as const, + }, + balanceNum: { gt: 0 }, + }, + }, + userStakedBalances: { + where: { + userAddress: { + equals: userAddress, + mode: 'insensitive' as const, + }, + balanceNum: { gt: 0 }, + }, + }, + }; + } } diff --git a/modules/pool/lib/pool-on-chain-data.service.ts b/modules/pool/lib/pool-on-chain-data.service.ts index 72bef5972..3957329eb 100644 --- a/modules/pool/lib/pool-on-chain-data.service.ts +++ b/modules/pool/lib/pool-on-chain-data.service.ts @@ -1,5 +1,5 @@ import { formatFixed } from '@ethersproject/bignumber'; -import { Chain, PrismaPoolType } from '@prisma/client'; +import { PrismaPoolType } from '@prisma/client'; import { isSameAddress } from '@balancer-labs/sdk'; import { prisma } from '../../../prisma/prisma-client'; import { isComposableStablePool, isStablePool } from './pool-utils'; @@ -15,6 +15,7 @@ const SUPPORTED_POOL_TYPES: PrismaPoolType[] = [ 'STABLE', 'META_STABLE', 'PHANTOM_STABLE', + 'COMPOSABLE_STABLE', 'LINEAR', 'LIQUIDITY_BOOTSTRAPPING', 'ELEMENT', @@ -33,7 +34,6 @@ export class PoolOnChainDataService { vaultAddress: networkContext.data.balancer.vault, yieldProtocolFeePercentage: networkContext.data.balancer.yieldProtocolFeePercentage, gyroConfig: networkContext.data.gyro?.config, - composableStableFactories: networkContext.data.balancer.composableStablePoolFactories, }; } @@ -95,19 +95,9 @@ export class PoolOnChainDataService { }); const gyroPools = filteredPools.filter((pool) => pool.type.includes('GYRO')); - const poolsWithComposableStableType = filteredPools.map((pool) => ({ - ...pool, - type: (isComposableStablePool(pool) ? 'COMPOSABLE_STABLE' : pool.type) as - | PrismaPoolType - | 'COMPOSABLE_STABLE', - })); const tokenPrices = await this.tokenService.getTokenPrices(); - const onchainResults = await fetchOnChainPoolData( - poolsWithComposableStableType, - this.options.vaultAddress, - 1024, - ); + const onchainResults = await fetchOnChainPoolData(filteredPools, this.options.vaultAddress, 1024); const gyroFees = await (this.options.gyroConfig ? fetchOnChainGyroFees(gyroPools, this.options.gyroConfig, 1024) : Promise.resolve({} as { [address: string]: string })); diff --git a/modules/pool/lib/pool-onchain-data.ts b/modules/pool/lib/pool-onchain-data.ts index 7a799a481..f5ca25b41 100644 --- a/modules/pool/lib/pool-onchain-data.ts +++ b/modules/pool/lib/pool-onchain-data.ts @@ -78,7 +78,7 @@ const getSwapFeeFn = (type: string) => { }; const getTotalSupplyFn = (type: PoolInput['type'], version: number) => { - if (['LINEAR', 'PHANTOM_STABLE'].includes(type)) { + if (['LINEAR'].includes(type) || (type === 'COMPOSABLE_STABLE' && version === 0)) { return 'getVirtualSupply'; } else if ( type === 'COMPOSABLE_STABLE' || diff --git a/modules/pool/lib/pool-utils.ts b/modules/pool/lib/pool-utils.ts index d1cc25d06..321575fd9 100644 --- a/modules/pool/lib/pool-utils.ts +++ b/modules/pool/lib/pool-utils.ts @@ -1,7 +1,4 @@ import { PrismaPoolDynamicData, PrismaPoolType } from '@prisma/client'; -import { isSameAddress } from '@balancer-labs/sdk'; -import { networkContext } from '../../network/network-context.service'; -import { prisma } from '../../../prisma/prisma-client'; type PoolWithTypeAndFactory = { address: string; @@ -16,21 +13,11 @@ export function isStablePool(poolType: PrismaPoolType) { } export function isWeightedPoolV2(pool: PoolWithTypeAndFactory) { - return ( - pool.type === 'WEIGHTED' && - networkContext.data.balancer.weightedPoolV2Factories.find((factory) => - isSameAddress(pool.factory || '', factory), - ) !== undefined - ); + return pool.type === 'WEIGHTED' && pool.version >= 2; } export function isComposableStablePool(pool: PoolWithTypeAndFactory) { - return ( - pool.type === 'PHANTOM_STABLE' && - networkContext.data.balancer.composableStablePoolFactories.find((factory) => - isSameAddress(pool.factory || '', factory), - ) !== undefined - ); + return pool.type === 'COMPOSABLE_STABLE' && pool.version > 0; } export function collectsYieldFee(pool: PoolWithTypeAndFactory) { diff --git a/modules/pool/pool.gql b/modules/pool/pool.gql index dc601f485..c1136124d 100644 --- a/modules/pool/pool.gql +++ b/modules/pool/pool.gql @@ -1,5 +1,5 @@ extend type Query { - poolGetPool(id: String!, chain: GqlChain): GqlPoolBase! + poolGetPool(id: String!, chain: GqlChain, userAddress: String): GqlPoolBase! poolGetPools( first: Int skip: Int @@ -55,7 +55,7 @@ extend type Mutation { poolBlackListAddPool(poolId: String!): String! poolBlackListRemovePool(poolId: String!): String! poolDeletePool(poolId: String!): String! - poolSyncAllPoolVersions: String! + poolSyncAllPoolTypesVersions: String! poolInitOnChainDataForAllPools: String! } @@ -74,7 +74,7 @@ type GqlPoolMinimal { allTokens: [GqlPoolTokenExpanded!]! dynamicData: GqlPoolDynamicData! staking: GqlPoolStaking - type: GqlPoolMinimalType! + type: GqlPoolType! version: Int! userBalance: GqlPoolUserBalance } @@ -88,11 +88,12 @@ type GqlPoolUserBalance { stakedBalanceUsd: Float! } -enum GqlPoolMinimalType { +enum GqlPoolType { WEIGHTED STABLE META_STABLE PHANTOM_STABLE + COMPOSABLE_STABLE ELEMENT LINEAR UNKNOWN @@ -108,7 +109,7 @@ interface GqlPoolBase { #fields that never change after creation id: ID! chain: GqlChain! - type: String! + type: GqlPoolType! version: Int! name: String! symbol: String! @@ -125,6 +126,8 @@ interface GqlPoolBase { dynamicData: GqlPoolDynamicData! staking: GqlPoolStaking + + userBalance: GqlPoolUserBalance } type GqlPoolDynamicData { @@ -187,7 +190,7 @@ type GqlPoolInvestOption { # we use an array here for invest options where there are more than one option, but only one can be selected # - FTM/wFTM or ETH/wETH - # - weighted boosted with nested phantom stable (bb-yv-USD) where you can only invest with DAI or USDC, not both at the same time + # - weighted boosted with nested composable stable (bb-yv-USD) where you can only invest with DAI or USDC, not both at the same time tokenOptions: [GqlPoolToken!]! } @@ -228,15 +231,17 @@ type GqlPoolWeighted implements GqlPoolBase { tokens: [GqlPoolTokenUnion!]! nestingType: GqlPoolNestingType! staking: GqlPoolStaking - type: String! + type: GqlPoolType! version: Int! + + userBalance: GqlPoolUserBalance } type GqlPoolGyro implements GqlPoolBase { id: ID! chain: GqlChain! name: String! - type: String! + type: GqlPoolType! symbol: String! address: Bytes! decimals: Int! @@ -270,8 +275,9 @@ type GqlPoolGyro implements GqlPoolBase { tokens: [GqlPoolTokenUnion!]! nestingType: GqlPoolNestingType! staking: GqlPoolStaking - type: String! version: Int! + + userBalance: GqlPoolUserBalance } type GqlPoolLiquidityBootstrapping implements GqlPoolBase { @@ -293,8 +299,10 @@ type GqlPoolLiquidityBootstrapping implements GqlPoolBase { tokens: [GqlPoolTokenUnion!]! nestingType: GqlPoolNestingType! staking: GqlPoolStaking - type: String! + type: GqlPoolType! version: Int! + + userBalance: GqlPoolUserBalance } type GqlPoolStable implements GqlPoolBase { @@ -316,8 +324,10 @@ type GqlPoolStable implements GqlPoolBase { tokens: [GqlPoolToken!]! amp: BigInt! staking: GqlPoolStaking - type: String! + type: GqlPoolType! version: Int! + + userBalance: GqlPoolUserBalance } type GqlPoolMetaStable implements GqlPoolBase { @@ -339,11 +349,13 @@ type GqlPoolMetaStable implements GqlPoolBase { tokens: [GqlPoolToken!]! amp: BigInt! staking: GqlPoolStaking - type: String! + type: GqlPoolType! version: Int! + + userBalance: GqlPoolUserBalance } -type GqlPoolPhantomStable implements GqlPoolBase { +type GqlPoolComposableStable implements GqlPoolBase { id: ID! chain: GqlChain! name: String! @@ -363,8 +375,10 @@ type GqlPoolPhantomStable implements GqlPoolBase { amp: BigInt! staking: GqlPoolStaking bptPriceRate: BigDecimal! - type: String! + type: GqlPoolType! version: Int! + + userBalance: GqlPoolUserBalance } type GqlPoolElement implements GqlPoolBase { @@ -389,8 +403,10 @@ type GqlPoolElement implements GqlPoolBase { principalToken: Bytes! baseToken: Bytes! staking: GqlPoolStaking - type: String! + type: GqlPoolType! version: Int! + + userBalance: GqlPoolUserBalance } type GqlPoolLinear implements GqlPoolBase { @@ -417,8 +433,10 @@ type GqlPoolLinear implements GqlPoolBase { lowerTarget: BigInt! staking: GqlPoolStaking bptPriceRate: BigDecimal! - type: String! + type: GqlPoolType! version: Int! + + userBalance: GqlPoolUserBalance } type GqlPoolLinearNested { @@ -441,11 +459,11 @@ type GqlPoolLinearNested { lowerTarget: BigInt! bptPriceRate: BigDecimal! - type: String! + type: GqlPoolType! version: Int! } -type GqlPoolPhantomStableNested { +type GqlPoolComposableStableNested { id: ID! name: String! symbol: String! @@ -453,7 +471,7 @@ type GqlPoolPhantomStableNested { owner: Bytes! factory: Bytes createTime: Int! - tokens: [GqlPoolTokenPhantomStableNestedUnion!]! + tokens: [GqlPoolTokenComposableStableNestedUnion!]! nestingType: GqlPoolNestingType! totalShares: BigDecimal! @@ -462,7 +480,7 @@ type GqlPoolPhantomStableNested { swapFee: BigDecimal! bptPriceRate: BigDecimal! - type: String! + type: GqlPoolType! version: Int! } @@ -471,14 +489,14 @@ union GqlPoolUnion = | GqlPoolStable | GqlPoolMetaStable | GqlPoolLinear - | GqlPoolPhantomStable + | GqlPoolComposableStable | GqlPoolElement | GqlPoolLiquidityBootstrapping | GqlPoolGyro -union GqlPoolNestedUnion = GqlPoolLinearNested | GqlPoolPhantomStableNested +union GqlPoolNestedUnion = GqlPoolLinearNested | GqlPoolComposableStableNested -union GqlPoolTokenUnion = GqlPoolToken | GqlPoolTokenPhantomStable | GqlPoolTokenLinear -union GqlPoolTokenPhantomStableNestedUnion = GqlPoolToken | GqlPoolTokenLinear +union GqlPoolTokenUnion = GqlPoolToken | GqlPoolTokenComposableStable | GqlPoolTokenLinear +union GqlPoolTokenComposableStableNestedUnion = GqlPoolToken | GqlPoolTokenLinear interface GqlPoolTokenBase { id: ID! @@ -530,7 +548,7 @@ type GqlPoolTokenLinear implements GqlPoolTokenBase { totalBalance: BigDecimal! } -type GqlPoolTokenPhantomStable implements GqlPoolTokenBase { +type GqlPoolTokenComposableStable implements GqlPoolTokenBase { id: ID! address: String! balance: BigDecimal! @@ -543,7 +561,7 @@ type GqlPoolTokenPhantomStable implements GqlPoolTokenBase { #the total balance in the pool, regardless of nesting totalBalance: BigDecimal! - pool: GqlPoolPhantomStableNested! + pool: GqlPoolComposableStableNested! } type GqlPoolLinearPoolData { @@ -560,7 +578,7 @@ type GqlPoolLinearPoolData { mainTokenTotalBalance: String! } -type GqlPoolStablePhantomPoolData { +type GqlPoolStableComposablePoolData { id: ID! address: String! symbol: String! @@ -634,8 +652,8 @@ input GqlPoolFilter { categoryNotIn: [GqlPoolFilterCategory!] tokensIn: [String!] tokensNotIn: [String!] - poolTypeIn: [GqlPoolFilterType!] - poolTypeNotIn: [GqlPoolFilterType!] + poolTypeIn: [GqlPoolType!] + poolTypeNotIn: [GqlPoolType!] idIn: [String!] idNotIn: [String!] filterIn: [String!] @@ -651,20 +669,20 @@ enum GqlPoolFilterCategory { BLACK_LISTED } -enum GqlPoolFilterType { - WEIGHTED - STABLE - META_STABLE - PHANTOM_STABLE - ELEMENT - LINEAR - UNKNOWN - LIQUIDITY_BOOTSTRAPPING - INVESTMENT - GYRO - GYRO3 - GYROE -} +# enum GqlPoolFilterType { +# WEIGHTED +# STABLE +# META_STABLE +# PHANTOM_STABLE +# ELEMENT +# LINEAR +# UNKNOWN +# LIQUIDITY_BOOTSTRAPPING +# INVESTMENT +# GYRO +# GYRO3 +# GYROE +# } type GqlPoolTokenExpanded { id: ID! diff --git a/modules/pool/pool.prisma b/modules/pool/pool.prisma index 6781f3727..c181de71a 100644 --- a/modules/pool/pool.prisma +++ b/modules/pool/pool.prisma @@ -47,6 +47,7 @@ enum PrismaPoolType { STABLE META_STABLE PHANTOM_STABLE + COMPOSABLE_STABLE ELEMENT LINEAR UNKNOWN @@ -229,6 +230,7 @@ model PrismaPoolTokenDynamicData { balanceUSD Float weight String? priceRate String + latestFxPrice Float? } model PrismaPoolSwap { diff --git a/modules/pool/pool.resolvers.ts b/modules/pool/pool.resolvers.ts index bb7387b93..45a36ee00 100644 --- a/modules/pool/pool.resolvers.ts +++ b/modules/pool/pool.resolvers.ts @@ -7,14 +7,14 @@ import { headerChain } from '../context/header-chain'; const balancerResolvers: Resolvers = { Query: { - poolGetPool: async (parent, { id, chain }, context) => { + poolGetPool: async (parent, { id, chain, userAddress }, context) => { const currentChain = headerChain(); if (!chain && currentChain) { chain = currentChain; } else if (!chain) { throw new Error('poolGetPool error: Provide "chain" param'); } - return poolService.getGqlPool(id, chain); + return poolService.getGqlPool(id, chain, userAddress ? userAddress : undefined); }, poolGetPools: async (parent, args, context) => { return poolService.getGqlPools(args); @@ -105,7 +105,7 @@ const balancerResolvers: Resolvers = { }, poolGetGyroPools: async () => { return poolService.getGqlGyroPools(); - } + }, }, Mutation: { poolSyncAllPoolsFromSubgraph: async (parent, {}, context) => { @@ -316,10 +316,10 @@ const balancerResolvers: Resolvers = { return 'success'; }, - poolSyncAllPoolVersions: async (parent, {}, context) => { + poolSyncAllPoolTypesVersions: async (parent, {}, context) => { isAdminRoute(context); - await poolService.syncPoolVersionForAllPools(); + await poolService.syncPoolTypeAndVersionForAllPools(); return 'success'; }, diff --git a/modules/pool/pool.service.ts b/modules/pool/pool.service.ts index 704d56364..83928c6cf 100644 --- a/modules/pool/pool.service.ts +++ b/modules/pool/pool.service.ts @@ -13,7 +13,6 @@ import { GqlPoolMinimal, GqlPoolSnapshotDataRange, GqlPoolUnion, - GqlPoolUserSwapVolume, QueryPoolGetBatchSwapsArgs, QueryPoolGetJoinExitsArgs, QueryPoolGetPoolsArgs, @@ -73,8 +72,8 @@ export class PoolService { return networkContext.services.balancerSubgraphService; } - public async getGqlPool(id: string, chain: GqlChain): Promise { - return this.poolGqlLoaderService.getPool(id, chain); + public async getGqlPool(id: string, chain: GqlChain, userAddress?: string): Promise { + return this.poolGqlLoaderService.getPool(id, chain, userAddress); } public async getGqlPools(args: QueryPoolGetPoolsArgs): Promise { @@ -358,24 +357,8 @@ export class PoolService { await this.poolSyncService.setPoolsWithPreferredGaugesAsIncentivized(); } - public async syncPoolVersionForAllPools() { - const subgraphPools = await this.balancerSubgraphService.getAllPools({}, false); - - for (const subgraphPool of subgraphPools) { - try { - await prisma.prismaPool.update({ - where: { id_chain: { chain: this.chain, id: subgraphPool.id } }, - data: { - version: subgraphPool.poolTypeVersion ? subgraphPool.poolTypeVersion : 1, - }, - }); - } catch (e: any) { - // Some pools are filtered from the DB, like test pools, - // so we just ignore them without breaking the loop - const error = e.meta ? e.meta.cause : e; - console.error(error, 'Network', networkContext.chain, 'Pool ID: ', subgraphPool.id); - } - } + public async syncPoolTypeAndVersionForAllPools() { + await this.poolCreatorService.updatePoolTypesAndVersionForAllPools(); } public async addToBlackList(poolId: string) { diff --git a/modules/protocol/protocol.gql b/modules/protocol/protocol.gql index c9af32d44..f1a2f2d8c 100644 --- a/modules/protocol/protocol.gql +++ b/modules/protocol/protocol.gql @@ -16,8 +16,6 @@ type GqlProtocolMetricsAggregated { swapFee24h: BigDecimal! swapVolume24h: BigDecimal! yieldCapture24h: BigDecimal! - swapFee7d: BigDecimal! - swapVolume7d: BigDecimal! numLiquidityProviders: BigInt! chains: [GqlProtocolMetricsChain!]! } @@ -31,8 +29,6 @@ type GqlProtocolMetricsChain { swapFee24h: BigDecimal! swapVolume24h: BigDecimal! yieldCapture24h: BigDecimal! - swapFee7d: BigDecimal! - swapVolume7d: BigDecimal! numLiquidityProviders: BigInt! } diff --git a/modules/protocol/protocol.service.ts b/modules/protocol/protocol.service.ts index 45af70a3f..0dc5cf39a 100644 --- a/modules/protocol/protocol.service.ts +++ b/modules/protocol/protocol.service.ts @@ -40,8 +40,6 @@ export class ProtocolService { const swapVolume24h = _.sumBy(chainMetrics, (metrics) => parseFloat(metrics.swapVolume24h)); const swapFee24h = _.sumBy(chainMetrics, (metrics) => parseFloat(metrics.swapFee24h)); const yieldCapture24h = _.sumBy(chainMetrics, (metrics) => parseFloat(metrics.yieldCapture24h)); - const swapVolume7d = _.sumBy(chainMetrics, (metrics) => parseFloat(metrics.swapVolume7d)); - const swapFee7d = _.sumBy(chainMetrics, (metrics) => parseFloat(metrics.swapFee7d)); const numLiquidityProviders = _.sumBy(chainMetrics, (metrics) => parseInt(metrics.numLiquidityProviders)); return { @@ -52,8 +50,6 @@ export class ProtocolService { swapVolume24h: `${swapVolume24h}`, swapFee24h: `${swapFee24h}`, yieldCapture24h: `${yieldCapture24h}`, - swapVolume7d: `${swapVolume7d}`, - swapFee7d: `${swapFee7d}`, numLiquidityProviders: `${numLiquidityProviders}`, chains: chainMetrics, }; @@ -71,8 +67,6 @@ export class ProtocolService { public async cacheProtocolMetrics(chain: Chain): Promise { const oneDayAgo = moment().subtract(24, 'hours').unix(); - const startOfDay = moment().startOf('day').unix(); - const sevenDayRange = moment().startOf('day').subtract(7, 'days').unix(); const client = new GraphQLClient(AllNetworkConfigsKeyedOnChain[chain].data.subgraphs.balancer); const subgraphClient = getSdk(client); @@ -115,15 +109,6 @@ export class ProtocolService { const yieldCapture24h = _.sumBy(pools, (pool) => (!pool.dynamicData ? 0 : pool.dynamicData.yieldCapture24h)); - //we take the aggregate of the last 7 days previous to today, since today's values grow throughout the day - const snapshotQueryResponse = await prisma.prismaPoolSnapshot.aggregate({ - _sum: { fees24h: true, volume24h: true }, - where: { - chain, - timestamp: { gte: sevenDayRange, lt: startOfDay }, - }, - }); - const balancerV1Tvl = await this.getBalancerV1Tvl(`${AllNetworkConfigsKeyedOnChain[chain].data.chain.id}`); const protocolData = { @@ -135,8 +120,6 @@ export class ProtocolService { swapVolume24h: `${swapVolume24h}`, swapFee24h: `${swapFee24h}`, yieldCapture24h: `${yieldCapture24h}`, - swapVolume7d: `${snapshotQueryResponse._sum.volume24h}`, - swapFee7d: `${snapshotQueryResponse._sum.fees24h}`, numLiquidityProviders: `${holdersQueryResponse._sum.holdersCount || '0'}`, }; diff --git a/modules/beethoven/balancer-sor.test.ts b/modules/sor/balancer-sor.test.ts similarity index 100% rename from modules/beethoven/balancer-sor.test.ts rename to modules/sor/balancer-sor.test.ts diff --git a/modules/sor/constants.ts b/modules/sor/constants.ts index 0fa69e5be..8c73cdc23 100644 --- a/modules/sor/constants.ts +++ b/modules/sor/constants.ts @@ -1,14 +1,14 @@ import { TokenAmount } from '@balancer/sdk'; import { GqlCowSwapApiResponse, GqlSorSwapType } from '../../schema'; -export const EMPTY_COWSWAP_RESPONSE = (assetIn: string, assetOut: string, amount: TokenAmount): GqlCowSwapApiResponse => { +export const EMPTY_COWSWAP_RESPONSE = ( + assetIn: string, + assetOut: string, + amount: TokenAmount, +): GqlCowSwapApiResponse => { return { - marketSp: '0', returnAmount: '0', - returnAmountConsideringFees: '0', - returnAmountFromSwaps: '0', swapAmount: amount.amount.toString(), - swapAmountForSwaps: '0', swaps: [], tokenAddresses: [], tokenIn: assetIn, @@ -352,4 +352,3 @@ export const poolsToIgnore = [ '0xbfd65c6160cfd638a85c645e6e6d8acac5dac935000000000000000000000004', '0xe274c9deb6ed34cfe4130f8d0a8a948dea5bb28600000000000000000000000d', ]; - diff --git a/modules/beethoven/balancer-sdk.gql b/modules/sor/sor.gql similarity index 93% rename from modules/beethoven/balancer-sdk.gql rename to modules/sor/sor.gql index 530ab20c2..9bee51655 100644 --- a/modules/beethoven/balancer-sdk.gql +++ b/modules/sor/sor.gql @@ -7,6 +7,13 @@ extend type Query { swapAmount: BigDecimal! #expected in human readable form swapOptions: GqlSorSwapOptionsInput! ): GqlSorGetSwapsResponse! + sorGetCowSwaps( + chain: GqlChain! + tokenIn: String! + tokenOut: String! + swapType: GqlSorSwapType! + swapAmount: BigDecimal! #expected in raw amount + ): GqlCowSwapApiResponse! sorGetBatchSwapForTokensIn( tokensIn: [GqlTokenAmountHumanReadable!]! tokenOut: String! @@ -14,6 +21,23 @@ extend type Query { ): GqlSorGetBatchSwapForTokensInResponse! } +type GqlCowSwapApiResponse { + tokenAddresses: [String!]! + swaps: [GqlSwap!]! + swapAmount: String! + returnAmount: String! + tokenIn: String! + tokenOut: String! +} + +type GqlSwap { + poolId: String! + assetInIndex: Int! + assetOutIndex: Int! + amount: String! + userData: String! +} + enum GqlSorSwapType { EXACT_IN EXACT_OUT @@ -109,24 +133,3 @@ type GqlSorGetBatchSwapForTokensInResponse { swaps: [GqlSorSwap!]! assets: [String!]! } - -type GqlCowSwapApiResponse { - tokenAddresses: [String!]! - swaps: [GqlSwap!]! - swapAmount: String! - swapAmountForSwaps: String! - returnAmount: String! - returnAmountFromSwaps: String! - returnAmountConsideringFees: String! - tokenIn: String! - tokenOut: String! - marketSp: String! -} - -type GqlSwap { - poolId: String! - assetInIndex: Int! - assetOutIndex: Int! - amount: String! - userData: String! -} diff --git a/modules/sor/sor.resolvers.ts b/modules/sor/sor.resolvers.ts new file mode 100644 index 000000000..31fb1c044 --- /dev/null +++ b/modules/sor/sor.resolvers.ts @@ -0,0 +1,31 @@ +import { Resolvers } from '../../schema'; +import { balancerSorService } from './sorV1Beets/balancer-sor.service'; +import { tokenService } from '../token/token.service'; +import { sorService } from './sor.service'; +import { getTokenAmountHuman } from './utils'; +import { headerChain } from '../context/header-chain'; + +const balancerSdkResolvers: Resolvers = { + Query: { + sorGetSwaps: async (parent, args, context) => { + const currentChain = headerChain(); + if (!args.chain && currentChain) { + args.chain = currentChain; + } else if (!args.chain) { + throw new Error('sorGetSwaps error: Provide "chain" param'); + } + + return sorService.getSorSwaps(args); + }, + sorGetBatchSwapForTokensIn: async (parent, args, context) => { + const tokens = await tokenService.getTokens(); + + return balancerSorService.getBatchSwapForTokensIn({ ...args, tokens }); + }, + sorGetCowSwaps: async (parent, args, context) => { + return sorService.getCowSwaps(args); + }, + }, +}; + +export default balancerSdkResolvers; diff --git a/modules/sor/sor.service.ts b/modules/sor/sor.service.ts index dc5ac98ce..f959b58d1 100644 --- a/modules/sor/sor.service.ts +++ b/modules/sor/sor.service.ts @@ -1,54 +1,91 @@ -import { GqlCowSwapApiResponse, GqlSorSwapType, GqlSorGetSwapsResponse, GqlSorSwapOptionsInput } from '../../schema'; -import { sorV1BalancerService } from './sorV1Balancer/sorV1Balancer.service'; +import { + GqlCowSwapApiResponse, + GqlSorSwapType, + GqlSorGetSwapsResponse, + QuerySorGetSwapsArgs, + QuerySorGetCowSwapsArgs, +} from '../../schema'; import { sorV1BeetsService } from './sorV1Beets/sorV1Beets.service'; import { sorV2Service } from './sorV2/sorV2.service'; -import { GetSwapsInput, SwapResult, SwapService } from './types'; +import { GetSwapsInput, SwapResult } from './types'; import { EMPTY_COWSWAP_RESPONSE } from './constants'; -import { publishMetric } from '../metrics/sor.metric'; import { Chain } from '@prisma/client'; import { parseUnits, formatUnits } from '@ethersproject/units'; import { tokenService } from '../token/token.service'; +import { getToken, getTokenAmountHuman, getTokenAmountRaw, zeroResponse } from './utils'; export class SorService { - async getCowSwaps(input: GetSwapsInput): Promise { - const swap = await this.getSwap({ ...input, swapOptions: {} }); - const emptyResponse = EMPTY_COWSWAP_RESPONSE(input.tokenIn, input.tokenOut, input.swapAmount); + async getCowSwaps(args: QuerySorGetCowSwapsArgs): Promise { + console.log('getCowSwaps args', JSON.stringify(args)); + const amountToken = args.swapType === 'EXACT_IN' ? args.tokenIn : args.tokenOut; + // Use TokenAmount to help follow scaling requirements in later logic + // args.swapAmount is RawScale, e.g. 1USDC should be passed as 1000000 + const amount = await getTokenAmountRaw(amountToken, args.swapAmount, args.chain!); + + const swap = await sorV2Service.getSwapResult({ + chain: args.chain!, + swapAmount: amount, + swapType: args.swapType, + tokenIn: args.tokenIn.toLowerCase(), + tokenOut: args.tokenOut.toLowerCase(), + swapOptions: {}, + }); + const emptyResponse = EMPTY_COWSWAP_RESPONSE(args.tokenIn, args.tokenOut, amount); if (!swap) return emptyResponse; try { // Updates with latest onchain data before returning - return await swap.getCowSwapResponse(input.chain, true); + return await swap.getCowSwapResponse(true); } catch (err) { console.log(`Error Retrieving QuerySwap`, err); return emptyResponse; } } - async getBeetsSwaps(input: GetSwapsInput): Promise { - console.log('getBeetsSwaps input', JSON.stringify(input)); - const swap = await this.getSwap(input, sorV1BeetsService); - const emptyResponse = sorV1BeetsService.zeroResponse( - input.swapType, - input.tokenIn, - input.tokenOut, - input.swapAmount, - ); + async getSorSwaps(args: QuerySorGetSwapsArgs): Promise { + console.log('getSorSwaps args', JSON.stringify(args)); + const tokenIn = args.tokenIn.toLowerCase(); + const tokenOut = args.tokenOut.toLowerCase(); + const amountToken = args.swapType === 'EXACT_IN' ? tokenIn : tokenOut; + const emptyResponse = zeroResponse(args.swapType, args.tokenIn, args.tokenOut, args.swapAmount); + + // check if tokens addresses exist + try { + await getToken(tokenIn, args.chain!); + await getToken(tokenOut, args.chain!); + } catch (e) { + console.log(e); + return emptyResponse; + } + + // Use TokenAmount to help follow scaling requirements in later logic + // args.swapAmount is HumanScale + const amount = await getTokenAmountHuman(amountToken, args.swapAmount, args.chain!); + + const swap = await this.getComparingSwap({ + chain: args.chain!, + swapAmount: amount, + swapOptions: args.swapOptions, + swapType: args.swapType, + tokenIn: tokenIn, + tokenOut: tokenOut, + }); if (!swap) return emptyResponse; try { // Updates with latest onchain data before returning - return swap.getBeetsSwapResponse(true); + return swap.getSorSwapResponse(true); } catch (err) { console.log(`Error Retrieving QuerySwap`, err); return emptyResponse; } } - private async getSwap(input: GetSwapsInput, v1Service: SwapService = sorV1BalancerService) { + private async getComparingSwap(input: GetSwapsInput) { const v1Start = +new Date(); - const swapV1 = await v1Service.getSwapResult(input); + const swapV1 = await sorV1BeetsService.getSwapResult(input); const v1Time = +new Date() - v1Start; const v2Start = +new Date(); @@ -122,9 +159,6 @@ export class SorService { v1Time: number, v2Time: number, ) { - // await publishMetric(chain, `SOR_VALID_V1`, v1.isValid ? 1 : 0); - // await publishMetric(chain, `SOR_VALID_V2`, v2.isValid ? 1 : 0); - if (!version) return; let v1ResultAmount = v1.inputAmount; @@ -154,10 +188,6 @@ export class SorService { let diff = bn(diffN.toFixed(decimals), decimals); let bestResultAmount = version === 'V1' ? v1ResultAmount : v2ResultAmount; - // await publishMetric(chain, `SOR_TIME_V1`, v1Time); - // await publishMetric(chain, `SOR_TIME_V2`, v2Time); - // await publishMetric(chain, `SOR_V2_PERFORMACE`, v2Perf); - console.log( [ 'SOR_RESULT', diff --git a/modules/sor/sorV1Balancer/sorV1Balancer.service.ts b/modules/sor/sorV1Balancer/sorV1Balancer.service.ts deleted file mode 100644 index f35d98914..000000000 --- a/modules/sor/sorV1Balancer/sorV1Balancer.service.ts +++ /dev/null @@ -1,135 +0,0 @@ -import axios from 'axios'; -import * as Sentry from '@sentry/node'; -import { AddressZero } from '@ethersproject/constants'; -import { Contract } from '@ethersproject/contracts'; -import { GqlSorSwapType, GqlCowSwapApiResponse, GqlSorGetSwapsResponse } from '../../../schema'; -import { GetSwapsInput, SwapService, SwapResult } from '../types'; -import { FundManagement, SwapTypes, SwapV2 } from '@balancer-labs/sdk'; -import { env } from '../../../app/env'; -import { AllNetworkConfigs, AllNetworkConfigsKeyedOnChain, chainToIdMap } from '../../network/network-config'; -import { DeploymentEnv } from '../../network/network-config-types'; - -import VaultAbi from '../../pool/abi/Vault.json'; -import { BigNumber } from 'ethers'; -import { TokenAmount } from '@balancer/sdk'; -import { Chain } from '@prisma/client'; - -type CowSwapSwapType = 'buy' | 'sell'; - -class SwapResultV1 implements SwapResult { - public inputAmount: bigint = BigInt(0); - public outputAmount: bigint = BigInt(0); - public isValid: boolean; - - constructor(private swap: GqlCowSwapApiResponse | null, private swapType: GqlSorSwapType) { - if (swap === null) { - this.isValid = false; - this.swap = null; - } else { - this.inputAmount = swapType === 'EXACT_IN' ? BigInt(swap.swapAmount) : BigInt(swap.returnAmount); - this.outputAmount = swapType === 'EXACT_IN' ? BigInt(swap.returnAmount) : BigInt(swap.swapAmount); - this.isValid = swap.swaps.length === 0 ? false : true; - } - } - - async getCowSwapResponse(chain: Chain, queryFirst = false): Promise { - if (!this.isValid || this.swap === null) throw new Error('No Response - Invalid Swap'); - - if (queryFirst) { - const swapType = this.mapSwapType(this.swapType); - const deltas = await this.queryBatchSwap(swapType, this.swap.swaps, this.swap.tokenAddresses, chain); - const tokenInAmount = deltas[this.swap.tokenAddresses.indexOf(this.swap.tokenIn)].toString(); - const tokenOutAmount = deltas[this.swap.tokenAddresses.indexOf(this.swap.tokenOut)].abs().toString(); - // console.log(`UPDATE:`, this.inputAmount, this.outputAmount, tokenInAmount, tokenOutAmount, deltas.toString()); - return { - ...this.swap, - returnAmount: swapType === SwapTypes.SwapExactIn ? tokenOutAmount : tokenInAmount, - swapAmount: swapType === SwapTypes.SwapExactIn ? tokenInAmount : tokenOutAmount, - }; - } - return this.swap; - } - - async getBeetsSwapResponse(queryFirst: boolean): Promise { - throw new Error('Use Beets service.'); - } - - private queryBatchSwap(swapType: SwapTypes, swaps: SwapV2[], assets: string[], chain: Chain): Promise { - const vault = AllNetworkConfigsKeyedOnChain[chain].data.balancer.vault; - const provider = AllNetworkConfigsKeyedOnChain[chain].provider; - - const vaultContract = new Contract(vault, VaultAbi, provider); - const funds: FundManagement = { - sender: AddressZero, - recipient: AddressZero, - fromInternalBalance: false, - toInternalBalance: false, - }; - - return vaultContract.queryBatchSwap(swapType, swaps, assets, funds); - } - - private mapSwapType(swapType: GqlSorSwapType): SwapTypes { - return swapType === 'EXACT_IN' ? SwapTypes.SwapExactIn : SwapTypes.SwapExactOut; - } -} -export class SorV1BalancerService implements SwapService { - public async getSwapResult({ chain, tokenIn, tokenOut, swapType, swapAmount }: GetSwapsInput): Promise { - try { - const swap = await this.querySorBalancer(chain, swapType, tokenIn, tokenOut, swapAmount); - return new SwapResultV1(swap, swapType); - } catch (err: any) { - console.error( - `SOR_V1_ERROR ${err.message} - tokenIn: ${tokenIn} - tokenOut: ${tokenOut} - swapAmount: ${swapAmount.amount} - swapType: ${swapType} - chain: ${chain}`, - ); - Sentry.captureException(err.message, { - tags: { - service: 'sorV1', - tokenIn, - tokenOut, - swapAmount: swapAmount.amount, - swapType, - chain, - }, - }); - return new SwapResultV1(null, swapType); - } - } - - /** - * Query Balancer API CowSwap/SOR endpoint. - * @param swapType - * @param tokenIn - * @param tokenOut - * @param swapAmountScaled - * @param swapOptions - * @returns - */ - private async querySorBalancer( - chain: Chain, - swapType: GqlSorSwapType, - tokenIn: string, - tokenOut: string, - swapAmount: TokenAmount, - ): Promise { - const chainId = chainToIdMap[chain]; - const endPoint = `https://api.balancer.fi/sor/${chainId}`; - const gasPrice = AllNetworkConfigs[chainId].data.sor[env.DEPLOYMENT_ENV as DeploymentEnv].gasPrice.toString(); - const swapData = { - orderKind: this.mapSwapType(swapType), - sellToken: tokenIn, - buyToken: tokenOut, - amount: swapAmount.amount.toString(), - gasPrice, - }; - - const { data } = await axios.post(endPoint, swapData); - return data; - } - - private mapSwapType(swapType: GqlSorSwapType): CowSwapSwapType { - return swapType === 'EXACT_IN' ? 'sell' : 'buy'; - } -} - -export const sorV1BalancerService = new SorV1BalancerService(); diff --git a/modules/beethoven/balancer-sor.service.ts b/modules/sor/sorV1Beets/balancer-sor.service.ts similarity index 95% rename from modules/beethoven/balancer-sor.service.ts rename to modules/sor/sorV1Beets/balancer-sor.service.ts index 7452f5ea0..19d5c2b3a 100644 --- a/modules/beethoven/balancer-sor.service.ts +++ b/modules/sor/sorV1Beets/balancer-sor.service.ts @@ -1,19 +1,19 @@ -import { GqlSorGetSwapsResponse, GqlSorSwapOptionsInput, GqlSorSwapType, GqlPoolMinimal } from '../../schema'; +import { GqlSorGetSwapsResponse, GqlSorSwapOptionsInput, GqlSorSwapType, GqlPoolMinimal } from '../../../schema'; import { formatFixed, parseFixed } from '@ethersproject/bignumber'; import { PrismaToken } from '@prisma/client'; -import { poolService } from '../pool/pool.service'; -import { oldBnum } from '../big-number/old-big-number'; +import { poolService } from '../../pool/pool.service'; +import { oldBnum } from '../../big-number/old-big-number'; import axios from 'axios'; import { FundManagement, SwapInfo, SwapTypes, SwapV2 } from '@balancer-labs/sdk'; -import { replaceEthWithZeroAddress, replaceZeroAddressWithEth } from '../web3/addresses'; +import { replaceEthWithZeroAddress, replaceZeroAddressWithEth } from '../../web3/addresses'; import { BigNumber } from 'ethers'; -import { TokenAmountHumanReadable } from '../common/global-types'; +import { TokenAmountHumanReadable } from '../../common/global-types'; import { AddressZero } from '@ethersproject/constants'; import { Contract } from '@ethersproject/contracts'; -import VaultAbi from '../pool/abi/Vault.json'; -import { env } from '../../app/env'; -import { networkContext } from '../network/network-context.service'; -import { DeploymentEnv } from '../network/network-config-types'; +import VaultAbi from '../../pool/abi/Vault.json'; +import { env } from '../../../app/env'; +import { networkContext } from '../../network/network-context.service'; +import { DeploymentEnv } from '../../network/network-config-types'; import * as Sentry from '@sentry/node'; import _ from 'lodash'; import { Logger } from '@ethersproject/logger'; @@ -332,7 +332,11 @@ export class BalancerSorService { } private getTokenDecimals(tokenAddress: string, tokens: PrismaToken[]): number { - if (tokenAddress === ZERO_ADDRESS || tokenAddress === NATIVE_ADDRESS) { + if ( + tokenAddress === ZERO_ADDRESS || + tokenAddress === NATIVE_ADDRESS || + tokenAddress === '0x0000000000000000000000000000000000001010' + ) { return 18; } diff --git a/modules/sor/sorV1Beets/sorV1Beets.service.ts b/modules/sor/sorV1Beets/sorV1Beets.service.ts index 067e5f87c..3b299996f 100644 --- a/modules/sor/sorV1Beets/sorV1Beets.service.ts +++ b/modules/sor/sorV1Beets/sorV1Beets.service.ts @@ -1,7 +1,7 @@ import { formatEther } from 'viem'; import { GqlSorSwapType, GqlCowSwapApiResponse, GqlSorGetSwapsResponse, GqlSorSwapOptionsInput } from '../../../schema'; import { GetSwapsInput, SwapService, SwapResult } from '../types'; -import { BalancerSorService } from '../../beethoven/balancer-sor.service'; +import { BalancerSorService } from './balancer-sor.service'; import { tokenService } from '../../token/token.service'; import { TokenAmount } from '@balancer/sdk'; @@ -23,11 +23,11 @@ class SwapResultV1 implements SwapResult { } } - async getCowSwapResponse(chain = 'MAINNET', queryFirst = false): Promise { + async getCowSwapResponse(queryFirst = false): Promise { throw new Error('Use Balancer Service'); } - async getBeetsSwapResponse(queryFirst: boolean): Promise { + async getSorSwapResponse(queryFirst: boolean): Promise { if (!this.isValid || this.swap === null) throw new Error('No Response - Invalid Swap'); // Beets service is already querying onchain return this.swap; @@ -42,7 +42,7 @@ export class SorV1BeetsService implements SwapService { public async getSwapResult(input: GetSwapsInput & { swapOptions: GqlSorSwapOptionsInput }): Promise { try { - const swap = await this.querySorBeets(input); + const swap = await this.querySorV1(input); return new SwapResultV1(swap, input.swapType); } catch (err) { console.log(`sorV1 Service Error`, err); @@ -59,7 +59,7 @@ export class SorV1BeetsService implements SwapService { return this.sorService.zeroResponse(swapType, tokenIn, tokenOut, formatEther(swapAmount.scale18)); } - private async querySorBeets( + private async querySorV1( input: GetSwapsInput & { swapOptions: GqlSorSwapOptionsInput }, ): Promise { const tokens = await tokenService.getTokens(); diff --git a/modules/sor/sorV2/sorV2.service.ts b/modules/sor/sorV2/sorV2.service.ts index a3b96fc2e..aed55dfd1 100644 --- a/modules/sor/sorV2/sorV2.service.ts +++ b/modules/sor/sorV2/sorV2.service.ts @@ -32,7 +32,7 @@ import { prisma } from '../../../prisma/prisma-client'; import { GetSwapsInput, SwapResult, SwapService } from '../types'; import { poolService } from '../../pool/pool.service'; import { tokenService } from '../../token/token.service'; -import { BalancerSorService } from '../../beethoven/balancer-sor.service'; +import { BalancerSorService } from '../sorV1Beets/balancer-sor.service'; import { env } from '../../../app/env'; import { DeploymentEnv } from '../../network/network-config-types'; import { Cache, CacheClass } from 'memory-cache'; @@ -49,43 +49,53 @@ const ALL_BASEPOOLS_CACHE_KEY = `basePools:all`; class SwapResultV2 implements SwapResult { private swap: SwapSdk | null; + private chain: Chain; public inputAmount: bigint = BigInt(0); public outputAmount: bigint = BigInt(0); public isValid: boolean; - constructor(swap: SwapSdk | null) { + constructor(swap: SwapSdk | null, chain: Chain) { if (swap === null) { this.isValid = false; this.swap = null; + this.chain = chain; } else { this.isValid = true; this.swap = swap; this.inputAmount = swap.inputAmount.amount; this.outputAmount = swap.outputAmount.amount; + this.chain = chain; } } - async getCowSwapResponse(chain: Chain, queryFirst = false): Promise { + async getCowSwapResponse(queryFirst = false): Promise { if (!this.isValid || this.swap === null) throw new Error('No Response - Invalid Swap'); if (!queryFirst) return this.mapResultToCowSwap(this.swap, this.swap.inputAmount, this.swap.outputAmount); else { - const rpcUrl = AllNetworkConfigsKeyedOnChain[chain].data.rpcUrl; - // Needs node >= 18 (https://github.com/wagmi-dev/viem/discussions/147) + const rpcUrl = AllNetworkConfigsKeyedOnChain[this.chain].data.rpcUrl; const updatedResult = await this.swap.query(rpcUrl); - // console.log(`UPDATE:`, this.swap.quote.amount.toString(), updatedResult.amount.toString()); - const ip = this.swap.swapKind === SwapKind.GivenIn ? this.swap.inputAmount : updatedResult; - const op = this.swap.swapKind === SwapKind.GivenIn ? updatedResult : this.swap.outputAmount; + const inputAmount = this.swap.swapKind === SwapKind.GivenIn ? this.swap.inputAmount : updatedResult; + const outputAmount = this.swap.swapKind === SwapKind.GivenIn ? updatedResult : this.swap.outputAmount; - return this.mapResultToCowSwap(this.swap, ip, op); + return this.mapResultToCowSwap(this.swap, inputAmount, outputAmount); } } - async getBeetsSwapResponse(queryFirst: boolean): Promise { + async getSorSwapResponse(queryFirst = false): Promise { if (!this.isValid || this.swap === null) throw new Error('No Response - Invalid Swap'); - return await this.mapResultToBeetsSwap(this.swap, this.swap.inputAmount, this.swap.outputAmount); + if (!queryFirst) return this.mapResultToBeetsSwap(this.swap, this.swap.inputAmount, this.swap.outputAmount); + else { + const rpcUrl = AllNetworkConfigsKeyedOnChain[this.chain].data.rpcUrl; + const updatedResult = await this.swap.query(rpcUrl); + + const inputAmount = this.swap.swapKind === SwapKind.GivenIn ? this.swap.inputAmount : updatedResult; + const outputAmount = this.swap.swapKind === SwapKind.GivenIn ? updatedResult : this.swap.outputAmount; + + return this.mapResultToBeetsSwap(this.swap, inputAmount, outputAmount); + } } private async mapResultToBeetsSwap( @@ -257,12 +267,8 @@ class SwapResultV2 implements SwapResult { const swapAmount = swap.swapKind === SwapKind.GivenIn ? inputAmount.amount.toString() : outputAmount.amount.toString(); return { - marketSp: '', // CowSwap is not using this field, confirmed. returnAmount, - returnAmountConsideringFees: returnAmount, // CowSwap is not using this field, confirmed. - returnAmountFromSwaps: returnAmount, // CowSwap is not using this field, confirmed. swapAmount, - swapAmountForSwaps: swapAmount, // CowSwap is not using this field, confirmed. swaps, tokenAddresses: swap.assets, tokenIn: swap.inputAmount.token.address, @@ -280,8 +286,9 @@ export class SorV2Service implements SwapService { public async getSwapResult( { chain, tokenIn, tokenOut, swapType, swapAmount, graphTraversalConfig }: GetSwapsInput, - maxNonBoostedPathDepth = 3, + maxNonBoostedPathDepth = 4, ): Promise { + const MAX_INCREASED_PATH_DEPTH = maxNonBoostedPathDepth + 1; try { const poolsFromDb = await this.getBasePools(chain); const tIn = await getToken(tokenIn as Address, chain); @@ -305,11 +312,17 @@ export class SorV2Service implements SwapService { maxNonBoostedPathDepth, ); const swap = await sorGetSwapsWithPools(tIn, tOut, swapKind, swapAmount, poolsFromDb, config); - if (!swap && maxNonBoostedPathDepth < 4) { + if (!swap && maxNonBoostedPathDepth < MAX_INCREASED_PATH_DEPTH) { return this.getSwapResult(arguments[0], maxNonBoostedPathDepth + 1); } - return new SwapResultV2(swap); + return new SwapResultV2(swap, chain); } catch (err: any) { + if ( + err.message.includes('No potential swap paths provided') && + maxNonBoostedPathDepth < MAX_INCREASED_PATH_DEPTH + ) { + return this.getSwapResult(arguments[0], maxNonBoostedPathDepth + 1); + } console.error( `SOR_V2_ERROR ${err.message} - tokenIn: ${tokenIn} - tokenOut: ${tokenOut} - swapAmount: ${swapAmount.amount} - swapType: ${swapType} - chain: ${chain}`, ); @@ -323,7 +336,7 @@ export class SorV2Service implements SwapService { chain, }, }); - return new SwapResultV2(null); + return new SwapResultV2(null, chain); } } @@ -354,6 +367,9 @@ export class SorV2Service implements SwapService { gt: 0.000000000001, }, swapEnabled: true, + totalLiquidity: { + gt: 50, + }, }, id: { notIn: [...poolIdsToExclude, ...poolsToIgnore], @@ -365,7 +381,7 @@ export class SorV2Service implements SwapService { 'ELEMENT', // not supported by b-sdk 'UNKNOWN', // not supported by b-sdk 'INVESTMENT', // not supported by b-sdk - 'FX', // TODO: FX pool tokens are missing latestFXPrice - needs to be added to the DB + 'FX', // needs more data ], }, AND: { @@ -511,6 +527,8 @@ export class SorV2Service implements SwapService { case PrismaPoolType.PHANTOM_STABLE: // Composablestables are PHANTOM_STABLE in Prisma. b-sdk treats Phantoms as ComposableStable. return 'ComposableStable'; + case PrismaPoolType.COMPOSABLE_STABLE: + return 'ComposableStable'; case PrismaPoolType.GYRO: return 'Gyro2'; case PrismaPoolType.GYRO3: diff --git a/modules/sor/types.ts b/modules/sor/types.ts index 564647c84..2059332ec 100644 --- a/modules/sor/types.ts +++ b/modules/sor/types.ts @@ -19,8 +19,8 @@ export interface GraphTraversalConfig { } export interface SwapResult { - getCowSwapResponse(chain: Chain, queryFirst: boolean): Promise; - getBeetsSwapResponse(queryFirst: boolean): Promise; + getCowSwapResponse(queryFirst: boolean): Promise; + getSorSwapResponse(queryFirst: boolean): Promise; isValid: boolean; outputAmount: bigint; inputAmount: bigint; diff --git a/modules/sor/utils.ts b/modules/sor/utils.ts index ab47ae2b2..ddc3ea77a 100644 --- a/modules/sor/utils.ts +++ b/modules/sor/utils.ts @@ -2,6 +2,8 @@ import { TokenAmount, Token, Address, NATIVE_ADDRESS, NATIVE_ASSETS } from '@bal import { tokenService } from '../token/token.service'; import { Chain } from '@prisma/client'; import { chainToIdMap } from '../network/network-config'; +import { GqlSorGetSwapsResponse, GqlSorSwapType } from '../../schema'; +import { replaceZeroAddressWithEth } from '../web3/addresses'; export async function getTokenAmountHuman(tokenAddr: string, humanAmount: string, chain: Chain): Promise { const token = await getToken(tokenAddr, chain); @@ -22,7 +24,11 @@ export async function getTokenAmountRaw(tokenAddr: string, rawAmount: string, ch export const getToken = async (tokenAddr: string, chain: Chain): Promise => { const chainId = Number(chainToIdMap[chain]); - if (tokenAddr === NATIVE_ADDRESS && chainId in NATIVE_ASSETS) { + // also check for the polygon native asset + if ( + (tokenAddr === NATIVE_ADDRESS || tokenAddr === '0x0000000000000000000000000000000000001010') && + chainId in NATIVE_ASSETS + ) { return NATIVE_ASSETS[chainId as keyof typeof NATIVE_ASSETS]; } else { const prismaToken = await tokenService.getToken(tokenAddr, chain); @@ -30,3 +36,32 @@ export const getToken = async (tokenAddr: string, chain: Chain): Promise return new Token(chainId, prismaToken.address as Address, prismaToken.decimals); } }; + +export const zeroResponse = ( + swapType: GqlSorSwapType, + tokenIn: string, + tokenOut: string, + swapAmount: string, +): GqlSorGetSwapsResponse => { + return { + marketSp: '0', + tokenAddresses: [], + swaps: [], + tokenIn: replaceZeroAddressWithEth(tokenIn), + tokenOut: replaceZeroAddressWithEth(tokenOut), + swapType, + tokenInAmount: swapType === 'EXACT_IN' ? swapAmount : '0', + tokenOutAmount: swapType === 'EXACT_IN' ? '0' : swapAmount, + swapAmount: swapType === 'EXACT_IN' ? '0' : swapAmount, + swapAmountScaled: '0', + swapAmountForSwaps: '0', + returnAmount: '0', + returnAmountScaled: '0', + returnAmountConsideringFees: '0', + returnAmountFromSwaps: '0', + routes: [], + effectivePrice: '0', + effectivePriceReversed: '0', + priceImpact: '0', + }; +}; diff --git a/modules/subgraphs/balancer-subgraph/balancer-subgraph-queries.graphql b/modules/subgraphs/balancer-subgraph/balancer-subgraph-queries.graphql index f154888d1..bdf1a2171 100644 --- a/modules/subgraphs/balancer-subgraph/balancer-subgraph-queries.graphql +++ b/modules/subgraphs/balancer-subgraph/balancer-subgraph-queries.graphql @@ -141,6 +141,7 @@ fragment BalancerToken on Token { id symbol address + latestFXPrice latestUSDPrice totalVolumeNotional totalVolumeUSD @@ -213,6 +214,9 @@ fragment BalancerPoolToken on PoolToken { weight priceRate index + token { + latestFXPrice + } } query BalancerPools( diff --git a/modules/token/latest-fx-price.ts b/modules/token/latest-fx-price.ts new file mode 100644 index 000000000..2eb090661 --- /dev/null +++ b/modules/token/latest-fx-price.ts @@ -0,0 +1,49 @@ +import { GraphQLClient } from 'graphql-request'; +import { getSdk } from '../subgraphs/balancer-subgraph/generated/balancer-subgraph-types'; +import { prisma } from '../../prisma/prisma-client'; +import { Chain } from '@prisma/client'; + +/** + * 'Latest FX Price' is relevant only to FX pools. It is sourced from offchain platforms, like Chainlink. + * The subgraph actively indexes this price by listening to the 'Answer Updated' events emitted by Chainlink oracles. + * For reference and more details, see the code at: + * https://github.com/balancer/balancer-subgraph-v2/blob/master/src/mappings/pricing.ts#L373 + * + * Note: 'LatestFXPrice' is a dependency of SORv2. + */ +export const syncLatestFXPrices = async (subgraphUrl: string, chain: Chain) => { + const { pools } = await fetchFxPools(subgraphUrl); + + for (const pool of pools) { + const { tokens } = pool; + if (!tokens) continue; + + for (const token of tokens) { + try { + await prisma.prismaPoolTokenDynamicData.update({ + where: { + id_chain: { + id: token.id, + chain, + }, + }, + data: { + latestFxPrice: token.token.latestFXPrice ? parseFloat(token.token.latestFXPrice) : undefined, + }, + }); + } catch (e) { + console.error(`Error updating latest FX price for token ${token.id} on chain ${chain}: ${e}`); + } + } + } + + return true; +}; + +const fetchFxPools = (subgraphUrl: string) => { + const sdk = getSdk(new GraphQLClient(subgraphUrl)); + + return sdk.BalancerPools({ + where: { poolType: 'FX' }, + }); +}; diff --git a/modules/token/token.gql b/modules/token/token.gql index 5481e271b..3adbb8db1 100644 --- a/modules/token/token.gql +++ b/modules/token/token.gql @@ -20,6 +20,7 @@ extend type Mutation { tokenReloadTokenPrices: Boolean tokenSyncTokenDefinitions: String! tokenSyncTokenDynamicData: String! + tokenSyncLatestFxPrices(chain: GqlChain!): String! tokenInitChartData(tokenAddress: String!): String! tokenDeletePrice(tokenAddress: String!, timestamp: Int!): Boolean! tokenDeleteTokenType(tokenAddress: String!, type: GqlTokenType!): String! diff --git a/modules/token/token.resolvers.ts b/modules/token/token.resolvers.ts index 8bc79db4c..7554d5c9c 100644 --- a/modules/token/token.resolvers.ts +++ b/modules/token/token.resolvers.ts @@ -3,6 +3,8 @@ import _ from 'lodash'; import { isAdminRoute } from '../auth/auth-context'; import { tokenService } from './token.service'; import { headerChain } from '../context/header-chain'; +import { syncLatestFXPrices } from './latest-fx-price'; +import { AllNetworkConfigsKeyedOnChain } from '../network/network-config'; const resolvers: Resolvers = { Query: { @@ -137,6 +139,14 @@ const resolvers: Resolvers = { return 'success'; }, + tokenSyncLatestFxPrices: async (parent, { chain }, context) => { + isAdminRoute(context); + const subgraphUrl = AllNetworkConfigsKeyedOnChain[chain].data.subgraphs.balancer; + + await syncLatestFXPrices(subgraphUrl, chain); + + return 'success'; + }, tokenInitChartData: async (parent, { tokenAddress }, context) => { isAdminRoute(context); diff --git a/modules/user/lib/user-sync-gauge-balance.service.ts b/modules/user/lib/user-sync-gauge-balance.service.ts index 0ee6df2f3..04b79bd9e 100644 --- a/modules/user/lib/user-sync-gauge-balance.service.ts +++ b/modules/user/lib/user-sync-gauge-balance.service.ts @@ -14,6 +14,30 @@ import { gaugeSubgraphService } from '../../subgraphs/gauge-subgraph/gauge-subgr import { AddressZero } from '@ethersproject/constants'; export class UserSyncGaugeBalanceService implements UserStakedBalanceService { + get chain() { + return networkContext.chain; + } + + get chainId() { + return networkContext.chainId; + } + + get provider() { + return networkContext.provider; + } + + get rpcUrl() { + return networkContext.data.rpcUrl; + } + + get rpcMaxBlockRange() { + return networkContext.data.rpcMaxBlockRange; + } + + get multicallAddress() { + return networkContext.data.multicall; + } + public async initStakedBalances(stakingTypes: PrismaPoolStakingType[]): Promise { if (!stakingTypes.includes('GAUGE')) { return; @@ -25,7 +49,7 @@ export class UserSyncGaugeBalanceService implements UserStakedBalanceService { console.log('initStakedBalances: loading pools...'); const pools = await prisma.prismaPool.findMany({ select: { id: true }, - where: { chain: networkContext.chain }, + where: { chain: this.chain }, }); const filteredGaugeShares = gaugeShares.filter((share) => { @@ -45,14 +69,14 @@ export class UserSyncGaugeBalanceService implements UserStakedBalanceService { data: userAddresses.map((userAddress) => ({ address: userAddress })), skipDuplicates: true, }), - prisma.prismaUserStakedBalance.deleteMany({ where: { chain: networkContext.chain } }), + prisma.prismaUserStakedBalance.deleteMany({ where: { chain: this.chain } }), prisma.prismaUserStakedBalance.createMany({ data: filteredGaugeShares.map((share) => { const pool = pools.find((pool) => pool.id === share.gauge.poolId); return { id: `${share.gauge.id}-${share.user.id}`, - chain: networkContext.chain, + chain: this.chain, balance: share.balance, balanceNum: parseFloat(share.balance), userAddress: share.user.id, @@ -63,8 +87,8 @@ export class UserSyncGaugeBalanceService implements UserStakedBalanceService { }), }), prisma.prismaUserBalanceSyncStatus.upsert({ - where: { type_chain: { type: 'STAKED', chain: networkContext.chain } }, - create: { type: 'STAKED', chain: networkContext.chain, blockNumber: block.number }, + where: { type_chain: { type: 'STAKED', chain: this.chain } }, + create: { type: 'STAKED', chain: this.chain, blockNumber: block.number }, update: { blockNumber: block.number }, }), ], @@ -77,7 +101,7 @@ export class UserSyncGaugeBalanceService implements UserStakedBalanceService { public async syncChangedStakedBalances(): Promise { // we always store the latest synced block const status = await prisma.prismaUserBalanceSyncStatus.findUnique({ - where: { type_chain: { type: 'STAKED', chain: networkContext.chain } }, + where: { type_chain: { type: 'STAKED', chain: this.chain } }, }); if (!status) { @@ -86,22 +110,24 @@ export class UserSyncGaugeBalanceService implements UserStakedBalanceService { const pools = await prisma.prismaPool.findMany({ include: { staking: true }, - where: { chain: networkContext.chain }, + where: { chain: this.chain }, }); + console.log(`user-sync-staked-balances-${this.chainId} got data from db.`); - console.log(`user-sync-staked-balances-${networkContext.chainId} got data from db.`); + const latestBlock = await this.provider.getBlockNumber(); + console.log(`user-sync-staked-balances-${this.chainId} got latest block.`); - const latestBlock = await networkContext.provider.getBlockNumber(); - console.log(`user-sync-staked-balances-${networkContext.chainId} got latest block.`); - const gaugeAddresses = await gaugeSubgraphService.getAllGaugeAddresses(); - console.log(`user-sync-staked-balances-${networkContext.chainId} got ${gaugeAddresses.length} gauges.`); + // Get gauge addresses + const gaugeAddresses = ( + await prisma.prismaPoolStakingGauge.findMany({ + select: { gaugeAddress: true }, + where: { chain: this.chain }, + }) + ).map((gauge) => gauge.gaugeAddress); // we sync at most 10k blocks at a time const startBlock = status.blockNumber + 1; - const endBlock = - latestBlock - startBlock > networkContext.data.rpcMaxBlockRange - ? startBlock + networkContext.data.rpcMaxBlockRange - : latestBlock; + const endBlock = latestBlock; // no new blocks have been minted, needed for slow networks if (startBlock > endBlock) { @@ -112,32 +138,58 @@ export class UserSyncGaugeBalanceService implements UserStakedBalanceService { we need to figure out which users have a changed balance on any gauge contract and update their balance, therefore we check all transfer events since the last synced block */ - - const events: ethers.providers.Log[] = []; - const logPromises: Promise[] = []; const erc20Interface = new ethers.utils.Interface(ERC20Abi); - console.log(`user-sync-staked-balances-${networkContext.chainId} getLogs of ${gaugeAddresses.length} gauges`); + // Split the range into smaller chunks to avoid RPC limits, setting up to 50 times max block range + const toBlock = Math.min(startBlock + 50 * this.rpcMaxBlockRange, latestBlock); + const range = toBlock - startBlock; + console.log(`user-sync-staked-balances-${this.chainId} block range from ${startBlock} to ${toBlock}`); + console.log(`user-sync-staked-balances-${this.chainId} getLogs for ${_.uniq(gaugeAddresses).length} gauges.`); + const events = await Promise.all( + // Getting logs in batches of max blocks allowed by RPC + Array.from({ length: Math.ceil(range / this.rpcMaxBlockRange) }, (_, i) => i).map(async (i) => { + const from = startBlock + i * this.rpcMaxBlockRange; + const to = Math.min(startBlock + (i + 1) * this.rpcMaxBlockRange, toBlock); - for (const gaugeAddress of gaugeAddresses) { - logPromises.push( - networkContext.provider.getLogs({ - //ERC20 Transfer topic - topics: ['0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'], - fromBlock: startBlock, - toBlock: endBlock, - address: gaugeAddress, - }), - ); - } - const allResponses = await Promise.all(logPromises); - console.log( - `user-sync-staked-balances-${networkContext.chainId} getLogs of ${gaugeAddresses.length} gauges done`, - ); + // Usually RPCs are handling any number of addresses, but it here batching just to be on the safe side + const logRequests: Promise[] = _.chunk(gaugeAddresses, 500).map((addresses) => { + // Fetch logs with a raw json request until we support Viem or Ethers6 + const payload = { + jsonrpc: '2.0', + id: 1, + method: 'eth_getLogs', + params: [ + { + address: addresses, + topics: [ethers.utils.id('Transfer(address,address,uint256)')], + fromBlock: '0x' + BigInt(from).toString(16), + toBlock: '0x' + BigInt(to).toString(16), + }, + ], + }; - for (const response of allResponses) { - events.push(...response); - } + return fetch(this.rpcUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }) + .then((response) => response.json() as Promise<{ result: ethers.providers.Log[] }>) + .then(({ result }) => result) + .catch((error) => { + console.error('Error fetching logs:', error); + return []; + }); + }); + + const events = await Promise.all(logRequests).then((res) => res.flat()); + + return events; + }), + ).then((res) => res.flat().filter((event) => event)); + + console.log(`user-sync-staked-balances-${this.chainId} getLogs for ${gaugeAddresses.length} gauges done`); const balancesToFetch = _.uniqBy( events @@ -153,13 +205,11 @@ export class UserSyncGaugeBalanceService implements UserStakedBalanceService { (entry) => entry.erc20Address + entry.userAddress, ); - console.log( - `user-sync-staked-balances-${networkContext.chainId} got ${balancesToFetch.length} balances to fetch.`, - ); + console.log(`user-sync-staked-balances-${this.chainId} got ${balancesToFetch.length} balances to fetch.`); if (balancesToFetch.length === 0) { await prisma.prismaUserBalanceSyncStatus.update({ - where: { type_chain: { type: 'STAKED', chain: networkContext.chain } }, + where: { type_chain: { type: 'STAKED', chain: this.chain } }, data: { blockNumber: endBlock }, }); @@ -167,14 +217,12 @@ export class UserSyncGaugeBalanceService implements UserStakedBalanceService { } const balances = await Multicaller.fetchBalances({ - multicallAddress: networkContext.data.multicall, - provider: networkContext.provider, + multicallAddress: this.multicallAddress, + provider: this.provider, balancesToFetch, }); - console.log( - `user-sync-staked-balances-${networkContext.chainId} got ${balancesToFetch.length} balances to fetch done.`, - ); + console.log(`user-sync-staked-balances-${this.chainId} got ${balancesToFetch.length} balances to fetch done.`); await prismaBulkExecuteOperations( [ @@ -193,7 +241,7 @@ export class UserSyncGaugeBalanceService implements UserStakedBalanceService { where: { id_chain: { id: `${userBalance.erc20Address}-${userBalance.userAddress}`, - chain: networkContext.chain, + chain: this.chain, }, }, update: { @@ -202,7 +250,7 @@ export class UserSyncGaugeBalanceService implements UserStakedBalanceService { }, create: { id: `${userBalance.erc20Address}-${userBalance.userAddress}`, - chain: networkContext.chain, + chain: this.chain, balance: formatFixed(userBalance.balance, 18), balanceNum: parseFloat(formatFixed(userBalance.balance, 18)), userAddress: userBalance.userAddress, @@ -216,7 +264,7 @@ export class UserSyncGaugeBalanceService implements UserStakedBalanceService { where: { type_chain: { type: 'STAKED', - chain: networkContext.chain, + chain: this.chain, }, }, data: { blockNumber: endBlock }, @@ -232,14 +280,14 @@ export class UserSyncGaugeBalanceService implements UserStakedBalanceService { const amount = formatFixed(balance, 18); await prisma.prismaUserStakedBalance.upsert({ - where: { id_chain: { id: `${staking.address}-${userAddress}`, chain: networkContext.chain } }, + where: { id_chain: { id: `${staking.address}-${userAddress}`, chain: this.chain } }, update: { balance: amount, balanceNum: parseFloat(amount), }, create: { id: `${staking.address}-${userAddress}`, - chain: networkContext.chain, + chain: this.chain, balance: amount, balanceNum: parseFloat(amount), userAddress: userAddress, diff --git a/modules/user/lib/user-sync-wallet-balance.service.ts b/modules/user/lib/user-sync-wallet-balance.service.ts index 340f39781..7389260aa 100644 --- a/modules/user/lib/user-sync-wallet-balance.service.ts +++ b/modules/user/lib/user-sync-wallet-balance.service.ts @@ -212,7 +212,7 @@ export class UserSyncWalletBalanceService { return events; }), - ).then((res) => res.flat()); + ).then((res) => res.flat().filter((event) => event)); console.log( `user-sync-wallet-balances-for-all-pools-${this.chainId} getLogs of ${poolAddresses.length} pools done`, diff --git a/modules/vebal/vebal-voting-list.service.ts b/modules/vebal/vebal-voting-list.service.ts index db30f16b9..02338e74d 100644 --- a/modules/vebal/vebal-voting-list.service.ts +++ b/modules/vebal/vebal-voting-list.service.ts @@ -42,7 +42,7 @@ export class VeBalVotingListService { chain: pool.chain, symbol: pool.symbol, address: pool.address, - type: pool.type, + type: pool.type === 'COMPOSABLE_STABLE' ? 'PHANTOM_STABLE' : pool.type, tokens: pool.tokens.map((token) => ({ address: token.address, weight: token.dynamicData?.weight, diff --git a/modules/vebal/vebal.gql b/modules/vebal/vebal.gql index 3c2d4c68b..a16cd12ce 100644 --- a/modules/vebal/vebal.gql +++ b/modules/vebal/vebal.gql @@ -20,7 +20,7 @@ type GqlVotingPool { chain: GqlChain! symbol: String! address: Bytes! - type: GqlPoolMinimalType! + type: GqlPoolType! tokens: [GqlVotingGaugeToken!]! diff --git a/modules/vebal/vebal.service.ts b/modules/vebal/vebal.service.ts index 841cd510b..0df5742f7 100644 --- a/modules/vebal/vebal.service.ts +++ b/modules/vebal/vebal.service.ts @@ -31,12 +31,18 @@ export class VeBalService { if (networkContext.data.veBal) { const veBalUsers = await prisma.prismaVeBalUserBalance.findMany({ where: { chain: networkContext.chain }, - orderBy: { balance: 'desc' }, }); - for (const user of veBalUsers) { + const veBalUsersNum = veBalUsers.map(user => ({ + ...user, + balance: parseFloat(user.balance), + })); + + veBalUsersNum.sort((a, b) => b.balance - a.balance); + + for (const user of veBalUsersNum) { if (user.userAddress === userAddress) { - balance = user.balance; + balance = user.balance.toString(); break; } rank++; diff --git a/prisma/migrations/20231120163312_add_composablestable_pooltype/migration.sql b/prisma/migrations/20231120163312_add_composablestable_pooltype/migration.sql new file mode 100644 index 000000000..78f3ee0b4 --- /dev/null +++ b/prisma/migrations/20231120163312_add_composablestable_pooltype/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "PrismaPoolType" ADD VALUE 'COMPOSABLE_STABLE'; diff --git a/prisma/migrations/20231215151625_latest_fx_price/migration.sql b/prisma/migrations/20231215151625_latest_fx_price/migration.sql new file mode 100644 index 000000000..3f97ae378 --- /dev/null +++ b/prisma/migrations/20231215151625_latest_fx_price/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "PrismaPoolTokenDynamicData" ADD COLUMN "latestFxPrice" DOUBLE PRECISION; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6e3103627..f7c1d2cf3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -93,6 +93,7 @@ enum PrismaPoolType { STABLE META_STABLE PHANTOM_STABLE + COMPOSABLE_STABLE ELEMENT LINEAR UNKNOWN @@ -275,6 +276,7 @@ model PrismaPoolTokenDynamicData { balanceUSD Float weight String? priceRate String + latestFxPrice Float? } model PrismaPoolSwap { diff --git a/worker/job-handlers.ts b/worker/job-handlers.ts index 9b6102541..d2b3d2143 100644 --- a/worker/job-handlers.ts +++ b/worker/job-handlers.ts @@ -14,6 +14,8 @@ import { veBalVotingListService } from '../modules/vebal/vebal-voting-list.servi import { cronsMetricPublisher } from '../modules/metrics/metrics.client'; import moment from 'moment'; import { cronsDurationMetricPublisher } from '../modules/metrics/cron-duration-metrics.client'; +import { syncLatestFXPrices } from '../modules/token/latest-fx-price'; +import { AllNetworkConfigs } from '../modules/network/network-config'; const runningJobs: Set = new Set(); @@ -278,6 +280,20 @@ export function configureWorkerRoutes(app: Express) { next, ); break; + case 'sync-latest-fx-prices': + await runIfNotAlreadyRunning( + job.name, + chainId, + () => { + const config = AllNetworkConfigs[chainId].data; + const subgraphUrl = config.subgraphs.balancer; + const chain = config.chain.prismaId; + return syncLatestFXPrices(subgraphUrl, chain); + }, + res, + next, + ); + break; default: res.sendStatus(400); throw new Error(`Unhandled job type ${job.name}`);