diff --git a/modules/network/mainnet.ts b/modules/network/mainnet.ts index ada1cc5d6..bac92f290 100644 --- a/modules/network/mainnet.ts +++ b/modules/network/mainnet.ts @@ -65,7 +65,7 @@ export const data: NetworkData = { rpcUrl: env.INFURA_API_KEY && (env.DEPLOYMENT_ENV as DeploymentEnv) === 'main' ? `https://mainnet.infura.io/v3/${env.INFURA_API_KEY}` - : 'https://rpc.eth.gateway.fm', + : 'https://eth.llamarpc.com', rpcMaxBlockRange: 700, protocolToken: 'bal', bal: { diff --git a/modules/sor/sor.gql b/modules/sor/sor.gql index e29bfd656..f4319547a 100644 --- a/modules/sor/sor.gql +++ b/modules/sor/sor.gql @@ -3,11 +3,29 @@ extend type Query { Get swap quote from the SOR, queries both the old and new SOR """ sorGetSwaps( - chain: GqlChain + """ + The Chain to query + """ + chain: GqlChain! + """ + Token address of the tokenIn + """ tokenIn: String! + """ + Token address of the tokenOut + """ tokenOut: String! + """ + SwapType either exact_in or exact_out (also givenIn or givenOut) + """ swapType: GqlSorSwapType! + """ + The amount to swap, in human form. + """ swapAmount: BigDecimal! #expected in human readable form + """ + Options for the swap + """ swapOptions: GqlSorSwapOptionsInput! ): GqlSorGetSwapsResponse! """ @@ -22,15 +40,36 @@ extend type Query { Token address of the tokenIn """ tokenIn: String! + """ + Token address of the tokenOut + """ tokenOut: String! + """ + SwapType either exact_in or exact_out (also givenIn or givenOut) + """ swapType: GqlSorSwapType! + """ + The amount to swap, in human form. + """ swapAmount: BigDecimal! #expected in human readable form - queryBatchSwap: Boolean #run queryBatchSwap to update with onchain values, default: true - useVaultVersion: Int #defaults that it gets the best swap from v2 and v3, can force to use only one vault version + """ + Whether to run queryBatchSwap to update with onchain values, default: true + """ + queryBatchSwap: Boolean + """ + Which vault version to use. If none provided, will chose the better return from either version + """ + useVaultVersion: Int ): GqlSorGetSwapPaths! } +""" +The swap paths for a swap +""" type GqlSorGetSwapPaths { + """ + The version of the vault these paths are from + """ vaultVersion: Int! """ The token address of the tokenIn provided @@ -40,26 +79,87 @@ type GqlSorGetSwapPaths { The token address of the tokenOut provided """ tokenOut: String! + """ + The swapType that was provided, exact_in vs exact_out (givenIn vs givenOut) + """ swapType: GqlSorSwapType! + """ + Swaps as needed for the vault swap input to execute the swap + """ swaps: [GqlSorSwap!]! #used by cowswap + """ + All token addresses (or assets) as needed for the vault swap input to execute the swap + """ + tokenAddresses: [String!]! #used by cowswap + """ + The found paths as needed as input for the b-sdk to execute the swap + """ paths: [GqlSorPath!]! #used by b-sdk + """ + The amount of tokenIn in human form + """ tokenInAmount: AmountHumanReadable! + """ + The amount of tokenOut in human form + """ tokenOutAmount: AmountHumanReadable! + """ + The swap amount in human form. Swap amount is either tokenInAmount (if swapType is exactIn) or tokenOutAmount (if swapType is exactOut) + """ swapAmount: AmountHumanReadable! - swapAmountScaled: BigDecimal! + """ + The swap amount in a raw form + """ + swapAmountRaw: BigDecimal! + """ + The return amount in human form. Return amount is either tokenOutAmount (if swapType is exactIn) or tokenInAmount (if swapType is exactOut) + """ returnAmount: AmountHumanReadable! - returnAmountScaled: BigDecimal! + """ + The return amount in a raw form + """ + returnAmountRaw: BigDecimal! + """ + The price of tokenOut in tokenIn. + """ effectivePrice: AmountHumanReadable! + """ + The price of tokenIn in tokenOut. + """ effectivePriceReversed: AmountHumanReadable! + """ + The swap routes including pool information. Used to display by the UI + """ routes: [GqlSorSwapRoute!]! + """ + Price impact in percent. 0.01 -> 0.01% + """ priceImpact: AmountHumanReadable! } +""" +A path of a swap. A swap can have multiple paths. Used as input to execute the swap via b-sdk +""" type GqlSorPath { + """ + Vault version of this path. + """ vaultVersion: Int! - pools: [String]! #list of pool Ids + """ + A sorted list of pool ids that are used in this path + """ + pools: [String]! + """ + A sorted list of tokens that are ussed in this path + """ tokens: [Token]! + """ + Output amount of this path in scaled form + """ outputAmountRaw: String! + """ + Input amount of this path in scaled form + """ inputAmountRaw: String! } @@ -133,35 +233,88 @@ type GqlSorGetSwapsResponse { priceImpact: AmountHumanReadable! } -#used by cowswap +""" +A single swap step as used for input to the vault to execute a swap +""" type GqlSorSwap { + """ + Pool id used in this swap step + """ poolId: String! + """ + Index of the asset used in the tokenAddress array. + """ assetInIndex: Int! + """ + Index of the asset used in the tokenAddress array. + """ assetOutIndex: Int! + """ + Amount to be swapped in this step. 0 for chained swap. + """ amount: String! + """ + UserData used in this swap, generally uses defaults. + """ userData: String! } +""" +The swap routes including pool information. Used to display by the UI +""" type GqlSorSwapRoute { + """ + Address of the tokenIn + """ tokenIn: String! - tokenInAmount: BigDecimal! + """ + Amount of the tokenIn in human form + """ + tokenInAmount: AmountHumanReadable! + """ + Address of the tokenOut + """ tokenOut: String! - tokenOutAmount: BigDecimal! + """ + Amount of the tokenOut in human form + """ + tokenOutAmount: AmountHumanReadable! + """ + Share of this route of the total swap + """ share: Float! + """ + The hops this route takes + """ hops: [GqlSorSwapRouteHop!]! } +""" +A hop of a route. A route can have many hops meaning it traverses more than one pool. +""" type GqlSorSwapRouteHop { + """ + Address of the tokenIn + """ tokenIn: String! - tokenInAmount: BigDecimal! + """ + Amount of the tokenIn in human form + """ + tokenInAmount: AmountHumanReadable! + """ + Address of the tokenOut + """ tokenOut: String! - tokenOutAmount: BigDecimal! + """ + Amount of the tokenOut in human form + """ + tokenOutAmount: AmountHumanReadable! + """ + The pool id of this hop. + """ poolId: String! + """ + The pool entity of this hop. + """ pool: GqlPoolMinimal! } - -type GqlSorGetBatchSwapForTokensInResponse { - tokenOutAmount: AmountHumanReadable! - swaps: [GqlSorSwap!]! - assets: [String!]! -} diff --git a/modules/sor/sor.service.ts b/modules/sor/sor.service.ts index 46adb6397..53ec0eb7e 100644 --- a/modules/sor/sor.service.ts +++ b/modules/sor/sor.service.ts @@ -6,7 +6,7 @@ import { GqlSorGetSwapPaths, } from '../../schema'; import { sorV1BeetsService } from './sorV1Beets/sorV1Beets.service'; -import { sorV2Service } from './sorV2/sorV2.service'; +import { sorV2Service } from './sorV2/sorPathService'; import { GetSwapsInput, SwapResult } from './types'; import * as Sentry from '@sentry/node'; import { Chain } from '@prisma/client'; @@ -74,7 +74,7 @@ export class SorService { // args.swapAmount is HumanScale const amount = await getTokenAmountHuman(amountToken, args.swapAmount, args.chain!); - const swap = await this.getComparingSwap({ + const swapResult = await this.getComparingSwap({ chain: args.chain!, swapAmount: amount, swapOptions: args.swapOptions, @@ -83,11 +83,13 @@ export class SorService { tokenOut: tokenOut, }); - if (!swap) return emptyResponse; + if (!swapResult) return emptyResponse; try { // Updates with latest onchain data before returning - return swap.getSorSwapResponse(args.swapOptions.queryBatchSwap ? args.swapOptions.queryBatchSwap : false); + return swapResult.getSorSwapResponse( + args.swapOptions.queryBatchSwap ? args.swapOptions.queryBatchSwap : false, + ); } catch (err) { console.log(`Error Retrieving QuerySwap`, err); return emptyResponse; diff --git a/modules/sor/sorV1Beets/balancer-sor.service.ts b/modules/sor/sorV1Beets/balancer-sor.service.ts index 9bd246f62..dfb0054ce 100644 --- a/modules/sor/sorV1Beets/balancer-sor.service.ts +++ b/modules/sor/sorV1Beets/balancer-sor.service.ts @@ -12,7 +12,7 @@ import { AllNetworkConfigsKeyedOnChain } from '../../network/network-config'; import { DeploymentEnv } from '../../network/network-config-types'; import _ from 'lodash'; import { SwapInfoRoute } from '@balancer-labs/sor'; -import { NATIVE_ADDRESS, ZERO_ADDRESS } from '@balancer/sdk'; +import { ZERO_ADDRESS } from '@balancer/sdk'; interface GetSwapsInput { tokenIn: string; @@ -228,7 +228,7 @@ export class BalancerSorService { private getTokenDecimals(tokenAddress: string, tokens: PrismaToken[]): number { if ( tokenAddress === ZERO_ADDRESS || - tokenAddress === NATIVE_ADDRESS || + tokenAddress === '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' || tokenAddress === '0x0000000000000000000000000000000000001010' ) { return 18; diff --git a/modules/sor/sorV2/beetsHelpers.test.ts b/modules/sor/sorV2/beetsHelpers.test.ts deleted file mode 100644 index 90e65eedc..000000000 --- a/modules/sor/sorV2/beetsHelpers.test.ts +++ /dev/null @@ -1,432 +0,0 @@ -import { GqlPoolMinimal, GqlSorSwapRoute } from '../../../schema'; -import { mapBatchSwap, mapRoutes, splitPaths } from './beetsHelpers'; -import { poolService } from '../../pool/pool.service'; -import { BatchSwapStep, SingleSwap, SwapKind } from './sor-port/types'; - -// npx jest --testPathPattern=modules/sor/sorV2/beetsHelpers.test.ts -describe('sorV2 Service - Routes', () => { - describe('SingleSwap', () => { - let singleSwap: SingleSwap; - let pools: GqlPoolMinimal[]; - let expectedRoute: GqlSorSwapRoute[]; - const assetIn = '0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4'; - const assetOut = '0xE4885Ed2818Cc9E840A25f94F9b2A28169D1AEA7'; - const assets = ['0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4', '0xE4885Ed2818Cc9E840A25f94F9b2A28169D1AEA7']; - - beforeAll(async () => { - pools = await poolService.getGqlPools({ - where: { idIn: ['0x216690738aac4aa0c4770253ca26a28f0115c595000000000000000000000b2c'] }, - }); - singleSwap = { - poolId: '0x216690738aac4aa0c4770253ca26a28f0115c595000000000000000000000b2c', - kind: SwapKind.GivenIn, - assetIn: '0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4', - assetOut: '0xE4885Ed2818Cc9E840A25f94F9b2A28169D1AEA7', - amount: BigInt(0), - userData: '0x', - }; - expectedRoute = [ - { - hops: [ - { - pool: pools[0], - poolId: '0x216690738aac4aa0c4770253ca26a28f0115c595000000000000000000000b2c', - tokenIn: '0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4', - tokenInAmount: '', - tokenOut: '0xE4885Ed2818Cc9E840A25f94F9b2A28169D1AEA7', - tokenOutAmount: '', - }, - ], - share: 1, - tokenIn: '0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4', - tokenInAmount: '', - tokenOut: '0xE4885Ed2818Cc9E840A25f94F9b2A28169D1AEA7', - tokenOutAmount: '', - }, - ]; - }); - test('GivenIn', () => { - const amountIn = '123456789112345678'; - const amountOut = '876543210987654321'; - singleSwap.kind = SwapKind.GivenIn; - singleSwap.amount = BigInt(amountIn); - expectedRoute[0].tokenInAmount = amountIn; - expectedRoute[0].tokenOutAmount = amountOut; - expectedRoute[0].hops[0].tokenInAmount = amountIn; - expectedRoute[0].hops[0].tokenOutAmount = amountOut; - const mappedRoute = mapRoutes( - singleSwap, - amountIn, - amountOut, - pools, - assetIn, - assetOut, - assets, - SwapKind.GivenIn, - ); - expect(mappedRoute).toEqual(expectedRoute); - }); - test('GivenOut', () => { - const amountIn = '876543210987654321'; - const amountOut = '123456789112345678'; - singleSwap.kind = SwapKind.GivenOut; - singleSwap.amount = BigInt(amountOut); - expectedRoute[0].tokenInAmount = amountIn; - expectedRoute[0].tokenOutAmount = amountOut; - expectedRoute[0].hops[0].tokenInAmount = amountIn; - expectedRoute[0].hops[0].tokenOutAmount = amountOut; - const mappedRoute = mapRoutes( - singleSwap, - amountIn, - amountOut, - pools, - assetIn, - assetOut, - assets, - SwapKind.GivenOut, - ); - expect(mappedRoute).toEqual(expectedRoute); - }); - }); - describe('BatchSwap', () => { - describe('ExactIn', () => { - let pools: GqlPoolMinimal[]; - let paths: BatchSwapStep[][]; - const assetIn = '0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4'; - const assetOut = '0x8159462d255C1D24915CB51ec361F700174cD994'; - const amountIn = '111111111111111111'; - const amountOut = '222222222222222222'; - const batchSwap: BatchSwapStep[] = [ - { - poolId: '0x216690738aac4aa0c4770253ca26a28f0115c595000000000000000000000b2c', - assetInIndex: BigInt(0), - assetOutIndex: BigInt(1), - amount: BigInt(amountIn), - userData: '0x', - }, - { - poolId: '0x8159462d255c1d24915cb51ec361f700174cd99400000000000000000000075d', - assetInIndex: BigInt(1), - assetOutIndex: BigInt(2), - amount: BigInt(0), - userData: '0x', - }, - ]; - const assets = [ - '0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4', - '0xE4885Ed2818Cc9E840A25f94F9b2A28169D1AEA7', - '0x8159462d255C1D24915CB51ec361F700174cD994', - ]; - const expectedRoute: GqlSorSwapRoute[] = [ - { - hops: [ - { - pool: {} as GqlPoolMinimal, - poolId: '0x216690738aac4aa0c4770253ca26a28f0115c595000000000000000000000b2c', - tokenIn: '0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4', - tokenInAmount: amountIn, - tokenOut: '0xE4885Ed2818Cc9E840A25f94F9b2A28169D1AEA7', - tokenOutAmount: '0', // TODO - Are we expecting to get the values for intermmediate steps? May need b-sdk to return that? - }, - { - pool: {} as GqlPoolMinimal, - poolId: '0x8159462d255c1d24915cb51ec361f700174cd99400000000000000000000075d', - tokenIn: '0xE4885Ed2818Cc9E840A25f94F9b2A28169D1AEA7', - tokenInAmount: '0', - tokenOut: '0x8159462d255C1D24915CB51ec361F700174cD994', - tokenOutAmount: amountOut, - }, - ], - share: 1, - tokenIn: '0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4', - tokenInAmount: amountIn, - tokenOut: '0x8159462d255C1D24915CB51ec361F700174cD994', - tokenOutAmount: amountOut, - }, - ]; - beforeAll(async () => { - pools = await poolService.getGqlPools({ - where: { - idIn: [ - '0x216690738aac4aa0c4770253ca26a28f0115c595000000000000000000000b2c', - '0x8159462d255c1d24915cb51ec361f700174cd99400000000000000000000075d', - ], - }, - }); - expectedRoute[0].hops[0].pool = pools.find( - (p) => p.id === '0x216690738aac4aa0c4770253ca26a28f0115c595000000000000000000000b2c', - ) as GqlPoolMinimal; - expectedRoute[0].hops[1].pool = pools.find( - (p) => p.id === '0x8159462d255c1d24915cb51ec361f700174cd99400000000000000000000075d', - ) as GqlPoolMinimal; - }); - describe('Single Path', () => { - test('splitPaths', () => { - paths = splitPaths(batchSwap, assetIn, assetOut, assets, SwapKind.GivenIn); - expect(paths.length).toEqual(1); - expect(paths[0].length).toEqual(2); - }); - test('mapBatchSwap', () => { - const route = mapBatchSwap(paths[0], amountIn, amountOut, SwapKind.GivenIn, assets, pools); - expect(route).toEqual(expectedRoute[0]); - }); - test('GivenIn', () => { - const mappedRoute = mapRoutes( - batchSwap, - amountIn, - amountOut, - pools, - assetIn, - assetOut, - assets, - SwapKind.GivenIn, - ); - expect(mappedRoute).toEqual(expectedRoute); - }); - }); - describe('Multiple Paths', () => { - beforeAll(() => { - batchSwap.push({ - poolId: '0x8159462d255c1d24915cb51ec361f700174cd99400000000000000000000075d', - assetInIndex: BigInt(0), - assetOutIndex: BigInt(2), - amount: BigInt(amountIn), - userData: '0x', - }); - expectedRoute[0].share = 0.5; - expectedRoute.push({ - hops: [ - { - pool: pools.find( - (p) => - p.id === '0x8159462d255c1d24915cb51ec361f700174cd99400000000000000000000075d', - ) as GqlPoolMinimal, - poolId: '0x8159462d255c1d24915cb51ec361f700174cd99400000000000000000000075d', - tokenIn: '0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4', - tokenInAmount: amountIn, - tokenOut: '0x8159462d255C1D24915CB51ec361F700174cD994', - tokenOutAmount: amountOut, - }, - ], - share: 0.5, - tokenIn: '0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4', - tokenInAmount: amountIn, - tokenOut: '0x8159462d255C1D24915CB51ec361F700174cD994', - tokenOutAmount: amountOut, - }); - }); - test('splitPaths', () => { - paths = splitPaths(batchSwap, assetIn, assetOut, assets, SwapKind.GivenIn); - expect(paths.length).toEqual(2); - expect(paths[0].length).toEqual(2); - expect(paths[1].length).toEqual(1); - }); - test('mapBatchSwap', () => { - const route = mapBatchSwap( - paths[0], - (BigInt(amountIn) * BigInt(2)).toString(), - (BigInt(amountOut) * BigInt(2)).toString(), - SwapKind.GivenIn, - assets, - pools, - ); - expect(route).toEqual(expectedRoute[0]); - }); - test('mapBatchSwap', () => { - const route = mapBatchSwap( - paths[1], - (BigInt(amountIn) * BigInt(2)).toString(), - (BigInt(amountOut) * BigInt(2)).toString(), - SwapKind.GivenIn, - assets, - pools, - ); - expect(route).toEqual(expectedRoute[1]); - }); - test('GivenIn', () => { - const mappedRoute = mapRoutes( - batchSwap, - (BigInt(amountIn) * BigInt(2)).toString(), - (BigInt(amountOut) * BigInt(2)).toString(), - pools, - assetIn, - assetOut, - assets, - SwapKind.GivenIn, - ); - expect(mappedRoute).toEqual(expectedRoute); - }); - }); - }); - describe('ExactOut', () => { - let pools: GqlPoolMinimal[]; - let paths: BatchSwapStep[][]; - const assetIn = '0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4'; - const assetOut = '0x8159462d255C1D24915CB51ec361F700174cD994'; - const amountIn = '111111111111111111'; - const amountOut = '222222222222222222'; - const batchSwap: BatchSwapStep[] = [ - { - poolId: '0x8159462d255c1d24915cb51ec361f700174cd99400000000000000000000075d', - assetInIndex: BigInt(1), - assetOutIndex: BigInt(2), - amount: BigInt(amountOut), - userData: '0x', - }, - { - poolId: '0x216690738aac4aa0c4770253ca26a28f0115c595000000000000000000000b2c', - assetInIndex: BigInt(0), - assetOutIndex: BigInt(1), - amount: BigInt(0), - userData: '0x', - }, - ]; - const assets = [ - '0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4', - '0xE4885Ed2818Cc9E840A25f94F9b2A28169D1AEA7', - '0x8159462d255C1D24915CB51ec361F700174cD994', - ]; - const expectedRoute: GqlSorSwapRoute[] = [ - { - hops: [ - { - pool: {} as GqlPoolMinimal, - poolId: '0x216690738aac4aa0c4770253ca26a28f0115c595000000000000000000000b2c', - tokenIn: '0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4', - tokenInAmount: amountIn, - tokenOut: '0xE4885Ed2818Cc9E840A25f94F9b2A28169D1AEA7', - tokenOutAmount: '0', // TODO - Are we expecting to get the values for intermmediate steps? May need b-sdk to return that? - }, - { - pool: {} as GqlPoolMinimal, - poolId: '0x8159462d255c1d24915cb51ec361f700174cd99400000000000000000000075d', - tokenIn: '0xE4885Ed2818Cc9E840A25f94F9b2A28169D1AEA7', - tokenInAmount: '0', - tokenOut: '0x8159462d255C1D24915CB51ec361F700174cD994', - tokenOutAmount: amountOut, - }, - ], - share: 1, - tokenIn: '0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4', - tokenInAmount: amountIn, - tokenOut: '0x8159462d255C1D24915CB51ec361F700174cD994', - tokenOutAmount: amountOut, - }, - ]; - - beforeAll(async () => { - pools = await poolService.getGqlPools({ - where: { - idIn: [ - '0x216690738aac4aa0c4770253ca26a28f0115c595000000000000000000000b2c', - '0x8159462d255c1d24915cb51ec361f700174cd99400000000000000000000075d', - ], - }, - }); - expectedRoute[0].hops[0].pool = pools.find( - (p) => p.id === '0x216690738aac4aa0c4770253ca26a28f0115c595000000000000000000000b2c', - ) as GqlPoolMinimal; - expectedRoute[0].hops[1].pool = pools.find( - (p) => p.id === '0x8159462d255c1d24915cb51ec361f700174cd99400000000000000000000075d', - ) as GqlPoolMinimal; - }); - describe('Single Path', () => { - test('split paths', () => { - paths = splitPaths(batchSwap, assetIn, assetOut, assets, SwapKind.GivenOut); - expect(paths.length).toEqual(1); - expect(paths[0].length).toEqual(2); - }); - test('mapBatchSwap', () => { - const route = mapBatchSwap(paths[0], amountIn, amountOut, SwapKind.GivenOut, assets, pools); - expect(route).toEqual(expectedRoute[0]); - }); - test('GivenOut', () => { - const mappedRoute = mapRoutes( - batchSwap, - amountIn, - amountOut, - pools, - assetIn, - assetOut, - assets, - SwapKind.GivenOut, - ); - expect(mappedRoute).toEqual(expectedRoute); - }); - }); - describe('Multi Paths', () => { - beforeAll(() => { - batchSwap.push({ - poolId: '0x8159462d255c1d24915cb51ec361f700174cd99400000000000000000000075d', - assetInIndex: BigInt(0), - assetOutIndex: BigInt(2), - amount: BigInt(amountOut), - userData: '0x', - }); - expectedRoute[0].share = 0.5; - expectedRoute.unshift({ - hops: [ - { - pool: pools.find( - (p) => - p.id === '0x8159462d255c1d24915cb51ec361f700174cd99400000000000000000000075d', - ) as GqlPoolMinimal, - poolId: '0x8159462d255c1d24915cb51ec361f700174cd99400000000000000000000075d', - tokenIn: '0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4', - tokenInAmount: amountIn, - tokenOut: '0x8159462d255C1D24915CB51ec361F700174cD994', - tokenOutAmount: amountOut, - }, - ], - share: 0.5, - tokenIn: '0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4', - tokenInAmount: amountIn, - tokenOut: '0x8159462d255C1D24915CB51ec361F700174cD994', - tokenOutAmount: amountOut, - }); - }); - test('splitPaths', () => { - paths = splitPaths(batchSwap, assetIn, assetOut, assets, SwapKind.GivenOut); - expect(paths.length).toEqual(2); - expect(paths[0].length).toEqual(1); - expect(paths[1].length).toEqual(2); - }); - test('mapBatchSwap', () => { - const route = mapBatchSwap( - paths[0], - (BigInt(amountIn) * BigInt(2)).toString(), - (BigInt(amountOut) * BigInt(2)).toString(), - SwapKind.GivenOut, - assets, - pools, - ); - expect(route).toEqual(expectedRoute[0]); - }); - test('mapBatchSwap', () => { - const route = mapBatchSwap( - paths[1], - (BigInt(amountIn) * BigInt(2)).toString(), - (BigInt(amountOut) * BigInt(2)).toString(), - SwapKind.GivenOut, - assets, - pools, - ); - expect(route).toEqual(expectedRoute[1]); - }); - test('GivenOut', () => { - const mappedRoute = mapRoutes( - batchSwap, - (BigInt(amountIn) * BigInt(2)).toString(), - (BigInt(amountOut) * BigInt(2)).toString(), - pools, - assetIn, - assetOut, - assets, - SwapKind.GivenOut, - ); - expect(mappedRoute).toEqual(expectedRoute); - }); - }); - }); - }); -}); diff --git a/modules/sor/sorV2/beetsHelpers.ts b/modules/sor/sorV2/beetsHelpers.ts deleted file mode 100644 index e0e9d24ed..000000000 --- a/modules/sor/sorV2/beetsHelpers.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { BatchSwapStep, NATIVE_ADDRESS, SingleSwap, Swap, SwapKind, ZERO_ADDRESS } from '@balancer/sdk'; -import { GqlPoolMinimal, GqlSorPath, GqlSorSwapRoute, GqlSorSwapRouteHop } from '../../../schema'; -import { formatFixed } from '@ethersproject/bignumber'; -import path from 'path'; - -export function mapRoutes( - swaps: BatchSwapStep[] | SingleSwap, - amountIn: string, - amountOut: string, - pools: GqlPoolMinimal[], - assetIn: string, - assetOut: string, - assets: string[], - kind: SwapKind, -): GqlSorSwapRoute[] { - const isBatchSwap = Array.isArray(swaps); - if (!isBatchSwap) { - const pool = pools.find((p) => p.id === swaps.poolId); - if (!pool) throw new Error('Pool not found while mapping route'); - return [mapSingleSwap(swaps, amountIn, amountOut, pool)]; - } - const paths = splitPaths(swaps, assetIn, assetOut, assets, kind); - return paths.map((p) => mapBatchSwap(p, amountIn, amountOut, kind, assets, pools)); -} - -export function mapPaths(swap: Swap): GqlSorPath[] { - const paths: GqlSorPath[] = []; - - for (const path of swap.paths) { - paths.push({ - vaultVersion: 2, - inputAmountRaw: path.inputAmount.amount.toString(), - outputAmountRaw: path.outputAmount.amount.toString(), - tokens: path.tokens.map((token) => ({ - address: token.address, - decimals: token.decimals, - })), - pools: path.pools.map((pool) => pool.id), - }); - } - - return paths; -} - -export function mapBatchSwap( - swaps: BatchSwapStep[], - amountIn: string, - amountOut: string, - kind: SwapKind, - assets: string[], - pools: GqlPoolMinimal[], -): GqlSorSwapRoute { - const exactIn = kind === SwapKind.GivenIn; - const first = swaps[0]; - const last = swaps[swaps.length - 1]; - let tokenInAmount: string; - let tokenOutAmount: string; - let share: bigint; - const one = BigInt(1e18); - if (exactIn) { - tokenInAmount = first.amount.toString(); - share = (BigInt(tokenInAmount) * one) / BigInt(amountIn); - tokenOutAmount = ((BigInt(amountOut) * share) / one).toString(); - } else { - tokenOutAmount = last.amount.toString(); - share = (BigInt(tokenOutAmount) * one) / BigInt(amountOut); - tokenInAmount = ((BigInt(amountIn) * share) / one).toString(); - } - - return { - tokenIn: assets[Number(first.assetInIndex)], - tokenOut: assets[Number(last.assetOutIndex)], - tokenInAmount, - tokenOutAmount, - share: Number(formatFixed(share.toString(), 18)), - hops: swaps.map((swap, i) => { - return { - tokenIn: assets[Number(swap.assetInIndex)], - tokenOut: assets[Number(swap.assetOutIndex)], - tokenInAmount: i === 0 ? tokenInAmount : '0', - tokenOutAmount: i === swaps.length - 1 ? tokenOutAmount : '0', - poolId: swap.poolId, - pool: pools.find((p) => p.id === swap.poolId) as GqlPoolMinimal, - }; - }), - }; -} - -export function splitPaths( - swaps: BatchSwapStep[], - assetIn: string, - assetOut: string, - assets: string[], - kind: SwapKind, -): BatchSwapStep[][] { - const swapsCopy = [...swaps]; - if (kind === SwapKind.GivenOut) { - swapsCopy.reverse(); - } - const assetInIndex = BigInt(assets.indexOf(assetIn === NATIVE_ADDRESS ? ZERO_ADDRESS : assetIn)); - const assetOutIndex = BigInt(assets.indexOf(assetOut === NATIVE_ADDRESS ? ZERO_ADDRESS : assetOut)); - let path: BatchSwapStep[]; - let paths: BatchSwapStep[][] = []; - swapsCopy.forEach((swap) => { - if (swap.assetInIndex === assetInIndex && swap.assetOutIndex === assetOutIndex) { - paths.push([swap]); - } else if (swap.assetInIndex === assetInIndex) { - path = [swap]; - } else if (swap.assetOutIndex === assetOutIndex) { - path.push(swap); - paths.push(path); - } else { - path.push(swap); - } - }); - return paths; -} - -function mapSingleSwap(swap: SingleSwap, amountIn: string, amountOut: string, pool: GqlPoolMinimal): GqlSorSwapRoute { - const hop: GqlSorSwapRouteHop = { - pool, - poolId: swap.poolId, - tokenIn: swap.assetIn, - tokenInAmount: amountIn, - tokenOut: swap.assetOut, - tokenOutAmount: amountOut, - }; - return { - share: 1, - tokenIn: swap.assetIn, - tokenOut: swap.assetOut, - tokenInAmount: amountIn, - tokenOutAmount: amountOut, - hops: [hop], - } as GqlSorSwapRoute; -} diff --git a/modules/sor/sorV2/lib/path.ts b/modules/sor/sorV2/lib/path.ts new file mode 100644 index 000000000..3376fb9bf --- /dev/null +++ b/modules/sor/sorV2/lib/path.ts @@ -0,0 +1,92 @@ +import { TokenAmount, SwapKind, Token } from '@balancer/sdk'; +import { BasePool } from './pools/basePool'; + +export class PathLocal { + public readonly pools: BasePool[]; + public readonly tokens: Token[]; + + public constructor(tokens: Token[], pools: BasePool[]) { + if (pools.length === 0 || tokens.length < 2) { + throw new Error('Invalid path: must contain at least 1 pool and 2 tokens.'); + } + if (tokens.length !== pools.length + 1) { + throw new Error('Invalid path: tokens length must equal pools length + 1'); + } + + this.pools = pools; + this.tokens = tokens; + } +} + +export class PathWithAmount extends PathLocal { + public readonly swapAmount: TokenAmount; + public readonly swapKind: SwapKind; + public readonly outputAmount: TokenAmount; + public readonly inputAmount: TokenAmount; + private readonly mutateBalances: boolean; + private readonly printPath: any = []; + + public constructor(tokens: Token[], pools: BasePool[], swapAmount: TokenAmount, mutateBalances?: boolean) { + super(tokens, pools); + this.swapAmount = swapAmount; + this.mutateBalances = Boolean(mutateBalances); + + //call to super ensures this array access is safe + if (tokens[0].isUnderlyingEqual(swapAmount.token)) { + this.swapKind = SwapKind.GivenIn; + } else { + this.swapKind = SwapKind.GivenOut; + } + + try { + if (this.swapKind === SwapKind.GivenIn) { + const amounts: TokenAmount[] = new Array(this.tokens.length); + amounts[0] = this.swapAmount; + for (let i = 0; i < this.pools.length; i++) { + const pool = this.pools[i]; + const outputAmount = pool.swapGivenIn( + this.tokens[i], + this.tokens[i + 1], + amounts[i], + this.mutateBalances, + ); + amounts[i + 1] = outputAmount; + this.printPath.push({ + pool: pool.id, + input: `${amounts[i].amount.toString()} ${this.tokens[i].symbol}`, + output: `${outputAmount.amount.toString()} ${this.tokens[i + 1].symbol}`, + }); + } + this.outputAmount = amounts[amounts.length - 1]; + this.inputAmount = this.swapAmount; + } else { + const amounts: TokenAmount[] = new Array(this.tokens.length); + amounts[amounts.length - 1] = this.swapAmount; + for (let i = this.pools.length; i >= 1; i--) { + const pool = this.pools[i - 1]; + const inputAmount = pool.swapGivenOut( + this.tokens[i - 1], + this.tokens[i], + amounts[i], + this.mutateBalances, + ); + amounts[i - 1] = inputAmount; + this.printPath.push({ + pool: pool.id, + input: `${inputAmount.amount.toString()} ${this.tokens[i - 1].symbol}`, + output: `${amounts[i].amount.toString()} ${this.tokens[i].symbol}`, + }); + } + this.printPath = this.printPath.reverse(); + this.inputAmount = amounts[0]; + this.outputAmount = this.swapAmount; + } + } catch { + throw new Error('Invalid path, swap amount exceeds maximum for pool'); + } + } + + public print(): void { + console.table(this.printPath); + } +} diff --git a/modules/sor/sorV2/lib/pathGraph/pathGraph.ts b/modules/sor/sorV2/lib/pathGraph/pathGraph.ts index 926e4b9c8..dc421b83c 100644 --- a/modules/sor/sorV2/lib/pathGraph/pathGraph.ts +++ b/modules/sor/sorV2/lib/pathGraph/pathGraph.ts @@ -1,5 +1,7 @@ -import { BasePool, Path, SwapKind, Token, TokenAmount } from '@balancer/sdk'; +import { SwapKind, Token, TokenAmount } from '@balancer/sdk'; import { PathGraphEdgeData, PathGraphTraversalConfig } from './pathGraphTypes'; +import { BasePool } from '../pools/basePool'; +import { PathLocal } from '../path'; const DEFAULT_MAX_PATHS_PER_TOKEN_PAIR = 2; @@ -61,7 +63,7 @@ export class PathGraph { tokenIn: Token; tokenOut: Token; graphTraversalConfig?: Partial; - }): Path[] { + }): PathLocal[] { // apply defaults, allowing caller override whatever they'd like const config: PathGraphTraversalConfig = { maxDepth: 6, diff --git a/modules/sor/sorV2/lib/pathGraph/pathGraphTypes.ts b/modules/sor/sorV2/lib/pathGraph/pathGraphTypes.ts index 1637b2ab2..6ae16c9d1 100644 --- a/modules/sor/sorV2/lib/pathGraph/pathGraphTypes.ts +++ b/modules/sor/sorV2/lib/pathGraph/pathGraphTypes.ts @@ -1,4 +1,5 @@ -import { BasePool, Token } from '@balancer/sdk'; +import { Token } from '@balancer/sdk'; +import { BasePool } from '../pools/basePool'; export interface PoolTokenPair { id: string; diff --git a/modules/sor/sorV2/lib/pools/basePool.ts b/modules/sor/sorV2/lib/pools/basePool.ts new file mode 100644 index 000000000..d2b7def79 --- /dev/null +++ b/modules/sor/sorV2/lib/pools/basePool.ts @@ -0,0 +1,14 @@ +import { PoolType, SwapKind, Token, TokenAmount } from '@balancer/sdk'; +import { Hex } from 'viem'; + +export interface BasePool { + readonly poolType: PoolType | string; + readonly id: Hex; + readonly address: string; + swapFee: bigint; + tokens: TokenAmount[]; + getNormalizedLiquidity(tokenIn: Token, tokenOut: Token): bigint; + swapGivenIn(tokenIn: Token, tokenOut: Token, swapAmount: TokenAmount, mutateBalances?: boolean): TokenAmount; + swapGivenOut(tokenIn: Token, tokenOut: Token, swapAmount: TokenAmount, mutateBalances?: boolean): TokenAmount; + getLimitAmountSwap(tokenIn: Token, tokenOut: Token, swapKind: SwapKind): bigint; +} diff --git a/modules/sor/sorV2/lib/pools/composableStable/composableStablePool.ts b/modules/sor/sorV2/lib/pools/composableStable/composableStablePool.ts index 272457f92..7d02d2a98 100644 --- a/modules/sor/sorV2/lib/pools/composableStable/composableStablePool.ts +++ b/modules/sor/sorV2/lib/pools/composableStable/composableStablePool.ts @@ -11,10 +11,11 @@ import { _calcTokenOutGivenExactBptIn, _calculateInvariant, } from './stableMath'; -import { BasePool, BigintIsh, PoolType, SwapKind, Token, TokenAmount } from '@balancer/sdk'; +import { BigintIsh, PoolType, SwapKind, Token, TokenAmount } from '@balancer/sdk'; import { chainToIdMap } from '../../../../../network/network-config'; import { StableData } from '../../../../../pool/subgraph-mapper'; import { TokenPairData } from '../../../../../pool/lib/pool-on-chain-tokenpair-data'; +import { BasePool } from '../basePool'; export class ComposableStablePoolToken extends TokenAmount { public readonly rate: bigint; diff --git a/modules/sor/sorV2/lib/pools/fx/fxPool.ts b/modules/sor/sorV2/lib/pools/fx/fxPool.ts index a0089458a..5eb12e94c 100644 --- a/modules/sor/sorV2/lib/pools/fx/fxPool.ts +++ b/modules/sor/sorV2/lib/pools/fx/fxPool.ts @@ -7,9 +7,10 @@ import { Chain } from '@prisma/client'; import { _calcInGivenOut, _calcOutGivenIn } from './fxMath'; import { RAY } from '../../utils/math'; import { FxPoolPairData } from './types'; -import { BasePool, PoolType, SwapKind, Token, TokenAmount } from '@balancer/sdk'; +import { PoolType, SwapKind, Token, TokenAmount } from '@balancer/sdk'; import { chainToIdMap } from '../../../../../network/network-config'; import { TokenPairData } from '../../../../../pool/lib/pool-on-chain-tokenpair-data'; +import { BasePool } from '../basePool'; const isUSDC = (address: string): boolean => { return ( diff --git a/modules/sor/sorV2/lib/pools/gyro2/gyro2Pool.ts b/modules/sor/sorV2/lib/pools/gyro2/gyro2Pool.ts index 3de14d818..2390fe09d 100644 --- a/modules/sor/sorV2/lib/pools/gyro2/gyro2Pool.ts +++ b/modules/sor/sorV2/lib/pools/gyro2/gyro2Pool.ts @@ -4,10 +4,11 @@ import { Chain } from '@prisma/client'; import { _calcInGivenOut, _calcOutGivenIn, _calculateInvariant, _findVirtualParams } from './gyro2Math'; import { MathSol, WAD } from '../../utils/math'; import { SWAP_LIMIT_FACTOR } from '../../utils/gyroHelpers/math'; -import { BasePool, BigintIsh, PoolType, SwapKind, Token, TokenAmount } from '@balancer/sdk'; +import { BigintIsh, PoolType, SwapKind, Token, TokenAmount } from '@balancer/sdk'; import { chainToIdMap } from '../../../../../network/network-config'; import { GyroData } from '../../../../../pool/subgraph-mapper'; import { TokenPairData } from '../../../../../pool/lib/pool-on-chain-tokenpair-data'; +import { BasePool } from '../basePool'; export class Gyro2PoolToken extends TokenAmount { public readonly index: number; diff --git a/modules/sor/sorV2/lib/pools/gyro3/gyro3Pool.ts b/modules/sor/sorV2/lib/pools/gyro3/gyro3Pool.ts index 1ab04963a..09b650248 100644 --- a/modules/sor/sorV2/lib/pools/gyro3/gyro3Pool.ts +++ b/modules/sor/sorV2/lib/pools/gyro3/gyro3Pool.ts @@ -4,10 +4,11 @@ import { Chain } from '@prisma/client'; import { MathSol, WAD } from '../../utils/math'; import { MathGyro, SWAP_LIMIT_FACTOR } from '../../utils/gyroHelpers/math'; import { _calcInGivenOut, _calcOutGivenIn, _calculateInvariant } from './gyro3Math'; -import { BasePool, BigintIsh, PoolType, SwapKind, Token, TokenAmount } from '@balancer/sdk'; +import { BigintIsh, PoolType, SwapKind, Token, TokenAmount } from '@balancer/sdk'; import { chainToIdMap } from '../../../../../network/network-config'; import { GyroData } from '../../../../../pool/subgraph-mapper'; import { TokenPairData } from '../../../../../pool/lib/pool-on-chain-tokenpair-data'; +import { BasePool } from '../basePool'; export class Gyro3PoolToken extends TokenAmount { public readonly index: number; diff --git a/modules/sor/sorV2/lib/pools/gyroE/gyroEPool.ts b/modules/sor/sorV2/lib/pools/gyroE/gyroEPool.ts index 81dc4086d..e79ade5ed 100644 --- a/modules/sor/sorV2/lib/pools/gyroE/gyroEPool.ts +++ b/modules/sor/sorV2/lib/pools/gyroE/gyroEPool.ts @@ -6,10 +6,11 @@ import { MathGyro, SWAP_LIMIT_FACTOR } from '../../utils/gyroHelpers/math'; import { DerivedGyroEParams, GyroEParams, Vector2 } from './types'; import { balancesFromTokenInOut, virtualOffset0, virtualOffset1 } from './gyroEMathHelpers'; import { calculateInvariantWithError, calcOutGivenIn, calcInGivenOut } from './gyroEMath'; -import { BasePool, BigintIsh, PoolType, SwapKind, Token, TokenAmount } from '@balancer/sdk'; +import { BigintIsh, PoolType, SwapKind, Token, TokenAmount } from '@balancer/sdk'; import { chainToIdMap } from '../../../../../network/network-config'; import { GyroData } from '../../../../../pool/subgraph-mapper'; import { TokenPairData } from '../../../../../pool/lib/pool-on-chain-tokenpair-data'; +import { BasePool } from '../basePool'; export class GyroEPoolToken extends TokenAmount { public readonly rate: bigint; diff --git a/modules/sor/sorV2/lib/pools/metastable/metastablePool.ts b/modules/sor/sorV2/lib/pools/metastable/metastablePool.ts index 281f04a2e..61e996660 100644 --- a/modules/sor/sorV2/lib/pools/metastable/metastablePool.ts +++ b/modules/sor/sorV2/lib/pools/metastable/metastablePool.ts @@ -4,10 +4,11 @@ import { ComposableStablePoolToken } from '../composableStable/composableStableP import { PrismaPoolWithDynamic } from '../../../../../../prisma/prisma-types'; import { _calcInGivenOut, _calcOutGivenIn, _calculateInvariant } from '../composableStable/stableMath'; import { MathSol, WAD } from '../../utils/math'; -import { BasePool, PoolType, SwapKind, Token, TokenAmount } from '@balancer/sdk'; +import { PoolType, SwapKind, Token, TokenAmount } from '@balancer/sdk'; import { chainToIdMap } from '../../../../../network/network-config'; import { StableData } from '../../../../../pool/subgraph-mapper'; import { TokenPairData } from '../../../../../pool/lib/pool-on-chain-tokenpair-data'; +import { BasePool } from '../basePool'; export class MetaStablePool implements BasePool { public readonly chain: Chain; diff --git a/modules/sor/sorV2/lib/pools/weighted/weightedPool.ts b/modules/sor/sorV2/lib/pools/weighted/weightedPool.ts index 320abea50..5ac4e0fa6 100644 --- a/modules/sor/sorV2/lib/pools/weighted/weightedPool.ts +++ b/modules/sor/sorV2/lib/pools/weighted/weightedPool.ts @@ -3,9 +3,10 @@ import { GqlPoolType } from '../../../../../../schema'; import { Chain } from '@prisma/client'; import { MathSol, WAD } from '../../utils/math'; import { Address, Hex, parseEther } from 'viem'; -import { BasePool, BigintIsh, SwapKind, Token, TokenAmount } from '@balancer/sdk'; +import { BigintIsh, SwapKind, Token, TokenAmount } from '@balancer/sdk'; import { chainToIdMap } from '../../../../../network/network-config'; import { TokenPairData } from '../../../../../pool/lib/pool-on-chain-tokenpair-data'; +import { BasePool } from '../basePool'; export class WeightedPoolToken extends TokenAmount { public readonly weight: bigint; diff --git a/modules/sor/sorV2/lib/router.ts b/modules/sor/sorV2/lib/router.ts index 89b975833..69013fda4 100644 --- a/modules/sor/sorV2/lib/router.ts +++ b/modules/sor/sorV2/lib/router.ts @@ -1,7 +1,9 @@ -import { BasePool, Path, PathWithAmount, SwapKind, Token, TokenAmount } from '@balancer/sdk'; +import { SwapKind, Token, TokenAmount } from '@balancer/sdk'; import { PathGraph } from './pathGraph/pathGraph'; import { PathGraphTraversalConfig } from './pathGraph/pathGraphTypes'; import { WAD } from './utils/math'; +import { BasePool } from './pools/basePool'; +import { PathLocal, PathWithAmount } from './path'; export class Router { private readonly pathGraph: PathGraph; @@ -15,7 +17,7 @@ export class Router { tokenOut: Token, pools: BasePool[], graphTraversalConfig?: Partial, - ): Path[] { + ): PathLocal[] { this.pathGraph.buildGraph({ pools }); const candidatePaths = this.pathGraph.getCandidatePaths({ @@ -27,7 +29,7 @@ export class Router { return candidatePaths; } - public getBestPaths(paths: Path[], swapKind: SwapKind, swapAmount: TokenAmount): PathWithAmount[] | null { + public getBestPaths(paths: PathLocal[], swapKind: SwapKind, swapAmount: TokenAmount): PathWithAmount[] | null { if (paths.length === 0) { throw new Error('No potential swap paths provided'); } diff --git a/modules/sor/sorV2/lib/static.ts b/modules/sor/sorV2/lib/static.ts index ccb0f99a0..6c52e43b3 100644 --- a/modules/sor/sorV2/lib/static.ts +++ b/modules/sor/sorV2/lib/static.ts @@ -7,8 +7,11 @@ import { FxPool } from './pools/fx/fxPool'; import { Gyro2Pool } from './pools/gyro2/gyro2Pool'; import { Gyro3Pool } from './pools/gyro3/gyro3Pool'; import { GyroEPool } from './pools/gyroE/gyroEPool'; -import { BasePool, Swap, SwapKind, SwapOptions, Token } from '@balancer/sdk'; +import { Swap, SwapKind, Token } from '@balancer/sdk'; import { ComposableStablePool } from './pools/composableStable/composableStablePool'; +import { BasePool } from './pools/basePool'; +import { SorSwapOptions } from './types'; +import { PathWithAmount } from './path'; export async function sorGetSwapsWithPools( tokenIn: Token, @@ -16,8 +19,8 @@ export async function sorGetSwapsWithPools( swapKind: SwapKind, swapAmountEvm: bigint, prismaPools: PrismaPoolWithDynamic[], - swapOptions?: Omit, -): Promise { + swapOptions?: Omit, +): Promise { const checkedSwapAmount = checkInputs(tokenIn, tokenOut, swapKind, swapAmountEvm); const basePools: BasePool[] = []; @@ -62,5 +65,5 @@ export async function sorGetSwapsWithPools( if (!bestPaths) return null; - return new Swap({ paths: bestPaths, swapKind }); + return bestPaths; } diff --git a/modules/sor/sorV2/lib/swapLocal.ts b/modules/sor/sorV2/lib/swapLocal.ts new file mode 100644 index 000000000..1bf91bacb --- /dev/null +++ b/modules/sor/sorV2/lib/swapLocal.ts @@ -0,0 +1,369 @@ +import { + BatchSwapStep, + DEFAULT_FUND_MANAGMENT, + DEFAULT_USERDATA, + PriceImpactAmount, + SingleSwap, + Slippage, + SwapKind, + TokenAmount, + ZERO_ADDRESS, + balancerQueriesAbi, + vaultV2Abi, +} from '@balancer/sdk'; +import { PathWithAmount } from './path'; +import { cloneDeep } from 'lodash'; +import { Address, Hex, createPublicClient, encodeFunctionData, getContract, http } from 'viem'; +import { MathSol, abs } from './utils/math'; +import { AllNetworkConfigsKeyedOnChain } from '../../../network/network-config'; +import { chainIdToChain } from '../../../network/chain-id-to-chain'; + +// A Swap can be a single or multiple paths +export class SwapLocal { + public constructor({ paths, swapKind }: { paths: PathWithAmount[]; swapKind: SwapKind }) { + if (paths.length === 0) throw new Error('Invalid swap: must contain at least 1 path.'); + + // paths with immutable pool balances + this.pathsImmutable = cloneDeep(paths); + + // Recalculate paths while mutating pool balances + this.paths = paths.map((path) => new PathWithAmount(path.tokens, path.pools, path.swapAmount, true)); + this.chainId = paths[0].tokens[0].chainId; + this.swapKind = swapKind; + this.isBatchSwap = paths.length > 1 || paths[0].pools.length > 1; + this.assets = [...new Set(paths.flatMap((p) => p.tokens).map((t) => t.address))]; + const swaps = this.getSwaps(this.paths); + + this.assets = this.assets.map((a) => { + return this.convertNativeAddressToZero(a); + }); + + this.swaps = swaps; + } + + public readonly chainId: number; + public readonly isBatchSwap: boolean; + public readonly paths: PathWithAmount[]; + public readonly pathsImmutable: PathWithAmount[]; + public readonly assets: Address[]; + public readonly swapKind: SwapKind; + public swaps: BatchSwapStep[] | SingleSwap; + + public get quote(): TokenAmount { + return this.swapKind === SwapKind.GivenIn ? this.outputAmount : this.inputAmount; + } + + public get inputAmount(): TokenAmount { + return this.getInputAmount(this.paths); + } + + public get outputAmount(): TokenAmount { + return this.getOutputAmount(this.paths); + } + + // rpcUrl is optional, but recommended to prevent rate limiting + public async query(rpcUrl?: string, block?: bigint): Promise { + const client = createPublicClient({ + transport: http(rpcUrl), + }); + + const queriesContract = getContract({ + address: AllNetworkConfigsKeyedOnChain[chainIdToChain[this.chainId]].data.balancer.v2 + .balancerQueriesAddress as Address, + abi: balancerQueriesAbi, + client, + }); + + let amount: TokenAmount; + if (this.isBatchSwap) { + const { result } = await queriesContract.simulate.queryBatchSwap( + [this.swapKind, this.swaps as BatchSwapStep[], this.assets, DEFAULT_FUND_MANAGMENT], + { + blockNumber: block, + }, + ); + + amount = + this.swapKind === SwapKind.GivenIn + ? TokenAmount.fromRawAmount( + this.outputAmount.token, + abs( + result[ + this.assets.indexOf(this.convertNativeAddressToZero(this.outputAmount.token.address)) + ], + ), + ) + : TokenAmount.fromRawAmount( + this.inputAmount.token, + abs( + result[ + this.assets.indexOf(this.convertNativeAddressToZero(this.inputAmount.token.address)) + ], + ), + ); + } else { + const { result } = await queriesContract.simulate.querySwap( + [this.swaps as SingleSwap, DEFAULT_FUND_MANAGMENT], + { blockNumber: block }, + ); + + amount = + this.swapKind === SwapKind.GivenIn + ? TokenAmount.fromRawAmount(this.outputAmount.token, result) + : TokenAmount.fromRawAmount(this.inputAmount.token, result); + } + + return amount; + } + + private convertNativeAddressToZero(address: Address): Address { + return address === AllNetworkConfigsKeyedOnChain[chainIdToChain[this.chainId]].data.eth.address + ? ZERO_ADDRESS + : address; + } + + public queryCallData(): string { + let callData: string; + if (this.isBatchSwap) { + callData = encodeFunctionData({ + abi: balancerQueriesAbi, + functionName: 'queryBatchSwap', + args: [this.swapKind, this.swaps as BatchSwapStep[], this.assets, DEFAULT_FUND_MANAGMENT], + }); + } else { + callData = encodeFunctionData({ + abi: balancerQueriesAbi, + functionName: 'querySwap', + args: [this.swaps as SingleSwap, DEFAULT_FUND_MANAGMENT], + }); + } + return callData; + } + + public get priceImpact(): PriceImpactAmount { + const paths = this.pathsImmutable; + + const pathsReverse = paths.map( + (path) => + new PathWithAmount( + [...path.tokens].reverse(), + [...path.pools].reverse(), + this.swapKind === SwapKind.GivenIn ? path.outputAmount : path.inputAmount, + ), + ); + + const amountInitial = + this.swapKind === SwapKind.GivenIn ? this.getInputAmount(paths).amount : this.getOutputAmount(paths).amount; + + const amountFinal = + this.swapKind === SwapKind.GivenIn + ? this.getOutputAmount(pathsReverse).amount + : this.getInputAmount(pathsReverse).amount; + + const priceImpact = MathSol.divDownFixed(abs(amountInitial - amountFinal), amountInitial * 2n); + return PriceImpactAmount.fromRawAmount(priceImpact); + } + + /** + * Takes a slippage acceptable by the user and returns the limits for a swap to be executed + * + * @param slippage slippage tolerance accepted by the user. Can be built using Slippage.fromPercentage() or any of its variations. + * @param expectedAmount is the amount that the user expects to receive or send, can be obtained from swap.query() + * @returns + */ + limits(slippage: Slippage, expectedAmount: TokenAmount): bigint[] { + const limits = new Array(this.assets.length).fill(0n); + let limitAmount: bigint; + if (this.swapKind === SwapKind.GivenIn) { + limitAmount = slippage.applyTo(expectedAmount.amount, -1); + } else { + limitAmount = slippage.applyTo(expectedAmount.amount); + } + + if (!this.isBatchSwap) { + return [limitAmount]; + } + + for (let i = 0; i < this.assets.length; i++) { + if ( + this.assets[i] === this.inputAmount.token.address || + (this.assets[i] === ZERO_ADDRESS && + this.inputAmount.token.address === + AllNetworkConfigsKeyedOnChain[chainIdToChain[this.chainId]].data.eth.address) + ) { + if (this.swapKind === SwapKind.GivenIn) { + limits[i] = this.inputAmount.amount; + } else { + limits[i] = limitAmount; + } + } + if ( + this.assets[i] === this.outputAmount.token.address || + (this.assets[i] === ZERO_ADDRESS && + this.outputAmount.token.address === + AllNetworkConfigsKeyedOnChain[chainIdToChain[this.chainId]].data.eth.address) + ) { + if (this.swapKind === SwapKind.GivenIn) { + limits[i] = -1n * limitAmount; + } else { + limits[i] = -1n * this.outputAmount.amount; + } + } + } + + return limits; + } + + /** + * Returns the transaction data to be sent to the vault contract + * + * @param limits calculated from swap.limits() + * @param deadline unix timestamp + * @param sender address of the sender + * @param recipient defaults to sender + * @returns + */ + transactionData(limits: bigint[], deadline: bigint, sender: Address, recipient = sender) { + return { + to: this.to(), + data: this.callData(limits, deadline, sender, recipient), + value: this.value(limits), + }; + } + + /** + * Returns the native assset value to be sent to the vault contract based on the swap kind and the limit amounts + * + * @param limits calculated from swap.limits() + * @returns + */ + private value(limits: bigint[]): bigint { + let value = 0n; + if ( + this.inputAmount.token.address === + AllNetworkConfigsKeyedOnChain[chainIdToChain[this.chainId]].data.eth.address + ) { + const idx = this.assets.indexOf(ZERO_ADDRESS); + value = limits[idx]; + } + return value; + } + + private to(): Address { + return AllNetworkConfigsKeyedOnChain[chainIdToChain[this.chainId]].data.balancer.v2.vaultAddress as Address; + } + + /** + * Returns the call data to be sent to the vault contract for the swap execution. + * + * @param limits calculated from swap.limits() + * @param deadline unix timestamp + * @param sender address of the sender + * @param recipient defaults to sender + * @returns + */ + private callData( + limits: bigint[], + deadline: bigint, + sender: Address, + recipient = sender, + internalBalances = { + to: false, + from: false, + }, + ): Hex { + let callData: Hex; + + const funds = { + sender, + recipient, + fromInternalBalance: internalBalances.from, + toInternalBalance: internalBalances.to, + }; + + if (this.isBatchSwap) { + callData = encodeFunctionData({ + abi: vaultV2Abi, + functionName: 'batchSwap', + args: [this.swapKind, this.swaps as BatchSwapStep[], this.assets, funds, limits, deadline], + }); + } else { + callData = encodeFunctionData({ + abi: vaultV2Abi, + functionName: 'swap', + args: [this.swaps as SingleSwap, funds, limits[0], deadline], + }); + } + + return callData; + } + + // public get executionPrice(): Price {} + + // helper methods + + private getSwaps(paths: PathWithAmount[]) { + let swaps: BatchSwapStep[] | SingleSwap; + if (this.isBatchSwap) { + swaps = [] as BatchSwapStep[]; + if (this.swapKind === SwapKind.GivenIn) { + paths.map((p) => { + p.pools.map((pool, i) => { + (swaps as BatchSwapStep[]).push({ + poolId: pool.id, + assetInIndex: BigInt(this.assets.indexOf(p.tokens[i].address)), + assetOutIndex: BigInt(this.assets.indexOf(p.tokens[i + 1].address)), + amount: i === 0 ? p.inputAmount.amount : 0n, + userData: DEFAULT_USERDATA, + }); + }); + }); + } else { + paths.map((p) => { + // Vault expects given out swaps to be in reverse order + const reversedPools = [...p.pools].reverse(); + const reversedTokens = [...p.tokens].reverse(); + reversedPools.map((pool, i) => { + (swaps as BatchSwapStep[]).push({ + poolId: pool.id, + assetInIndex: BigInt(this.assets.indexOf(reversedTokens[i + 1].address)), + assetOutIndex: BigInt(this.assets.indexOf(reversedTokens[i].address)), + amount: i === 0 ? p.outputAmount.amount : 0n, + userData: DEFAULT_USERDATA, + }); + }); + }); + } + } else { + const path = this.paths[0]; + const pool = path.pools[0]; + const assetIn = this.convertNativeAddressToZero(path.tokens[0].address); + const assetOut = this.convertNativeAddressToZero(path.tokens[1].address); + swaps = { + poolId: pool.id, + kind: this.swapKind, + assetIn, + assetOut, + amount: path.swapAmount.amount, + userData: DEFAULT_USERDATA, + } as SingleSwap; + } + return swaps; + } + + private getInputAmount(paths: PathWithAmount[]): TokenAmount { + if (!paths.every((p) => p.inputAmount.token.isEqual(paths[0].inputAmount.token))) { + throw new Error('Input amount can only be calculated if all paths have the same input token'); + } + const amounts = paths.map((path) => path.inputAmount); + return amounts.reduce((a, b) => a.add(b)); + } + + private getOutputAmount(paths: PathWithAmount[]): TokenAmount { + if (!paths.every((p) => p.outputAmount.token.isEqual(paths[0].outputAmount.token))) { + throw new Error('Output amount can only be calculated if all paths have the same output token'); + } + const amounts = paths.map((path) => path.outputAmount); + return amounts.reduce((a, b) => a.add(b)); + } +} diff --git a/modules/sor/sorV2/lib/types.ts b/modules/sor/sorV2/lib/types.ts index 6125ed6af..dbc14224f 100644 --- a/modules/sor/sorV2/lib/types.ts +++ b/modules/sor/sorV2/lib/types.ts @@ -1,16 +1,16 @@ import { PathGraphTraversalConfig } from './pathGraph/pathGraphTypes'; -export interface SwapOptions { +interface FundManagement { + sender: string; + fromInternalBalance: boolean; + recipient: string; + toInternalBalance: boolean; +} + +export interface SorSwapOptions { block?: bigint; slippage?: bigint; funds?: FundManagement; deadline?: bigint; graphTraversalConfig?: Partial; } - -export interface FundManagement { - sender: string; - fromInternalBalance: boolean; - recipient: string; - toInternalBalance: boolean; -} diff --git a/modules/sor/sorV2/lib/utils/helpers.ts b/modules/sor/sorV2/lib/utils/helpers.ts index d8886f994..04db28828 100644 --- a/modules/sor/sorV2/lib/utils/helpers.ts +++ b/modules/sor/sorV2/lib/utils/helpers.ts @@ -1,4 +1,15 @@ -import { BigintIsh, SwapKind, Token, TokenAmount } from '@balancer/sdk'; +import { + BatchSwapStep, + BigintIsh, + DEFAULT_USERDATA, + PriceImpactAmount, + SingleSwap, + SwapKind, + Token, + TokenAmount, +} from '@balancer/sdk'; +import { PathWithAmount } from '../path'; +import { MathSol, abs } from './math'; export function checkInputs( tokenIn: Token, @@ -23,3 +34,38 @@ export function checkInputs( return amount; } + +export function calculatePriceImpact(paths: PathWithAmount[], swapKind: SwapKind): PriceImpactAmount { + const pathsReverse = paths.map( + (path) => + new PathWithAmount( + [...path.tokens].reverse(), + [...path.pools].reverse(), + swapKind === SwapKind.GivenIn ? path.outputAmount : path.inputAmount, + ), + ); + + const amountInitial = swapKind === SwapKind.GivenIn ? getInputAmount(paths).amount : getOutputAmount(paths).amount; + + const amountFinal = + swapKind === SwapKind.GivenIn ? getOutputAmount(pathsReverse).amount : getInputAmount(pathsReverse).amount; + + const priceImpact = MathSol.divDownFixed(abs(amountInitial - amountFinal), amountInitial * 2n); + return PriceImpactAmount.fromRawAmount(priceImpact); +} + +export function getInputAmount(paths: PathWithAmount[]): TokenAmount { + if (!paths.every((p) => p.inputAmount.token.isEqual(paths[0].inputAmount.token))) { + throw new Error('Input amount can only be calculated if all paths have the same input token'); + } + const amounts = paths.map((path) => path.inputAmount); + return amounts.reduce((a, b) => a.add(b)); +} + +export function getOutputAmount(paths: PathWithAmount[]): TokenAmount { + if (!paths.every((p) => p.outputAmount.token.isEqual(paths[0].outputAmount.token))) { + throw new Error('Output amount can only be calculated if all paths have the same output token'); + } + const amounts = paths.map((path) => path.outputAmount); + return amounts.reduce((a, b) => a.add(b)); +} diff --git a/modules/sor/sorV2/sorPathService.ts b/modules/sor/sorV2/sorPathService.ts new file mode 100644 index 000000000..b6425e6e7 --- /dev/null +++ b/modules/sor/sorV2/sorPathService.ts @@ -0,0 +1,428 @@ +import { + GqlPoolMinimal, + GqlSorGetSwapPaths, + GqlSorPath, + GqlSorSwap, + GqlSorSwapRoute, + GqlSorSwapRouteHop, + GqlSorSwapType, +} from '../../../schema'; +import { Chain } from '@prisma/client'; +import { PrismaPoolWithDynamic, prismaPoolWithDynamic } from '../../../prisma/prisma-types'; +import { prisma } from '../../../prisma/prisma-client'; +import { GetSwapsInput, GetSwapsV2Input as GetSwapPathsInput, SwapResult, SwapService } from '../types'; +import { poolsToIgnore } from '../constants'; +import { AllNetworkConfigsKeyedOnChain, chainToIdMap } from '../../network/network-config'; +import * as Sentry from '@sentry/node'; +import { Address, formatUnits } from 'viem'; +import { sorGetSwapsWithPools as sorGetPathsWithPools } from './lib/static'; +import { SwapResultV2 } from './swapResultV2'; +import { poolService } from '../../pool/pool.service'; +import { replaceZeroAddressWithEth } from '../../web3/addresses'; +import { getToken, swapPathsZeroResponse } from '../utils'; +import { BatchSwapStep, DEFAULT_USERDATA, SingleSwap, Swap, SwapKind } from '@balancer/sdk'; +import { PathWithAmount } from './lib/path'; +import { calculatePriceImpact, getInputAmount, getOutputAmount } from './lib/utils/helpers'; +import { SwapLocal } from './lib/swapLocal'; + +class SorPathService implements SwapService { + // This is only used for the old SOR service + public async getSwapResult( + { chain, tokenIn, tokenOut, swapType, swapAmount, graphTraversalConfig }: GetSwapsInput, + maxNonBoostedPathDepth = 4, + ): Promise { + try { + const poolsFromDb = await this.getBasePoolsFromDb(chain); + const tIn = await getToken(tokenIn as Address, chain); + const tOut = await getToken(tokenOut as Address, chain); + const swapKind = this.mapSwapTypeToSwapKind(swapType); + const config = graphTraversalConfig + ? { + graphTraversalConfig: { + maxNonBoostedPathDepth, + ...graphTraversalConfig, + }, + } + : { + graphTraversalConfig: { + maxNonBoostedPathDepth, + }, + }; + const paths = await sorGetPathsWithPools(tIn, tOut, swapKind, swapAmount.amount, poolsFromDb, config); + if (!paths && maxNonBoostedPathDepth < 5) { + return this.getSwapResult(arguments[0], maxNonBoostedPathDepth + 1); + } + if (!paths) { + return new SwapResultV2(null, chain); + } + + const swap = new SwapLocal({ paths: paths, swapKind }); + + return new SwapResultV2(swap, chain); + } catch (err: any) { + console.error( + `SOR_V2_ERROR ${err.message} - tokenIn: ${tokenIn} - tokenOut: ${tokenOut} - swapAmount: ${swapAmount.amount} - swapType: ${swapType} - chain: ${chain}`, + ); + Sentry.captureException(err.message, { + tags: { + service: 'sorV2', + tokenIn, + tokenOut, + swapAmount: swapAmount.amount, + swapType, + chain, + }, + }); + return new SwapResultV2(null, chain); + } + } + + // The new SOR service + public async getSorSwapPaths(input: GetSwapPathsInput, maxNonBoostedPathDepth = 4): Promise { + const paths = await this.getSwapPathsFromSor(input, maxNonBoostedPathDepth); + const emptyResponse = swapPathsZeroResponse(input.tokenIn, input.tokenOut); + + if (!paths) { + return emptyResponse; + } + + try { + return this.mapToSorSwapPaths(paths!, input.swapType, input.chain, input.queryBatchSwap); + } catch (err: any) { + console.log(`Error Retrieving QuerySwap`, err); + Sentry.captureException(err.message, { + tags: { + service: 'sorV2 query swap', + tokenIn: input.tokenIn, + tokenOut: input.tokenOut, + swapAmount: formatUnits(input.swapAmount.amount, input.swapAmount.token.decimals), + swapType: input.swapType, + chain: input.chain, + }, + }); + return emptyResponse; + } + } + + private async getSwapPathsFromSor( + { chain, tokenIn, tokenOut, swapType, swapAmount, graphTraversalConfig }: GetSwapPathsInput, + maxNonBoostedPathDepth = 4, + ): Promise { + try { + const poolsFromDb = await this.getBasePoolsFromDb(chain); + const tIn = await getToken(tokenIn as Address, chain); + const tOut = await getToken(tokenOut as Address, chain); + const swapKind = this.mapSwapTypeToSwapKind(swapType); + const config = graphTraversalConfig + ? { + graphTraversalConfig: { + maxNonBoostedPathDepth, + ...graphTraversalConfig, + }, + } + : { + graphTraversalConfig: { + maxNonBoostedPathDepth, + }, + }; + const paths = await sorGetPathsWithPools(tIn, tOut, swapKind, swapAmount.amount, poolsFromDb, config); + // if we dont find a path with depth 4, we try one more level. + if (!paths && maxNonBoostedPathDepth < 5) { + return this.getSwapPathsFromSor(arguments[0], maxNonBoostedPathDepth + 1); + } + return paths; + } catch (err: any) { + console.error( + `SOR_V2_ERROR ${err.message} - tokenIn: ${tokenIn} - tokenOut: ${tokenOut} - swapAmount: ${swapAmount.amount} - swapType: ${swapType} - chain: ${chain}`, + ); + Sentry.captureException(err.message, { + tags: { + service: 'sorV2', + tokenIn, + tokenOut, + swapAmount: swapAmount.amount, + swapType, + chain, + }, + }); + return null; + } + } + + // map the SOR output to the required response type + private async mapToSorSwapPaths( + paths: PathWithAmount[], + swapType: GqlSorSwapType, + chain: Chain, + queryFirst = false, + ): Promise { + const swapKind = this.mapSwapTypeToSwapKind(swapType); + + // TODO for v3 we need to update per swap path + let updatedAmount; + if (queryFirst) { + const sdkSwap = new Swap({ + chainId: parseFloat(chainToIdMap[chain]), + paths: paths.map((path) => ({ + vaultVersion: 2 as 2 | 3, + inputAmountRaw: path.inputAmount.amount, + outputAmountRaw: path.outputAmount.amount, + tokens: path.tokens.map((token) => ({ + address: token.address, + decimals: token.decimals, + })), + pools: path.pools.map((pool) => pool.id), + })), + swapKind, + }); + + updatedAmount = await sdkSwap.query(AllNetworkConfigsKeyedOnChain[chain].data.rpcUrl); + } + + let inputAmount = getInputAmount(paths); + let outputAmount = getOutputAmount(paths); + + // only total inputAmount or outputAmount is updated. We can't inputAmount or outputAmount per path. + // this means that also subsequent calcs dont take the updatedAmount into account, i.e. priceImpact, paths and routes + if (updatedAmount) { + inputAmount = swapKind === SwapKind.GivenIn ? inputAmount : updatedAmount; + outputAmount = swapKind === SwapKind.GivenIn ? updatedAmount : outputAmount; + } + + // price impact does not take the updatedAmount into account + const priceImpact = calculatePriceImpact(paths, swapKind).decimal.toFixed(4); + + // get all affected pools + let poolIds: string[] = []; + for (const path of paths) { + poolIds.push(...path.pools.map((pool) => pool.id)); + } + const pools = await poolService.getGqlPools({ + where: { idIn: poolIds }, + }); + + const sorPaths: GqlSorPath[] = []; + for (const path of paths) { + // paths used as input for b-sdk for client + sorPaths.push({ + vaultVersion: 2, + inputAmountRaw: path.inputAmount.amount.toString(), + outputAmountRaw: path.outputAmount.amount.toString(), + tokens: path.tokens.map((token) => ({ + address: token.address, + decimals: token.decimals, + })), + pools: path.pools.map((pool) => pool.id), + }); + } + + const returnAmount = swapKind === SwapKind.GivenIn ? outputAmount : inputAmount; + const swapAmount = swapKind === SwapKind.GivenIn ? inputAmount : outputAmount; + + const effectivePrice = inputAmount.divDownFixed(outputAmount.scale18); + const effectivePriceReversed = outputAmount.divDownFixed(inputAmount.scale18); + + return { + vaultVersion: 2, + paths: sorPaths, + swapType, + swaps: this.mapSwaps(paths, swapKind), + tokenAddresses: [...new Set(paths.flatMap((p) => p.tokens).map((t) => t.address))], + tokenIn: replaceZeroAddressWithEth(inputAmount.token.address), + tokenOut: replaceZeroAddressWithEth(outputAmount.token.address), + tokenInAmount: inputAmount.amount.toString(), + tokenOutAmount: outputAmount.amount.toString(), + swapAmount: formatUnits(swapAmount.amount, swapAmount.token.decimals), + swapAmountRaw: swapAmount.amount.toString(), + returnAmount: formatUnits(returnAmount.amount, returnAmount.token.decimals), + returnAmountRaw: returnAmount.amount.toString(), + effectivePrice: formatUnits(effectivePrice.amount, effectivePrice.token.decimals), + effectivePriceReversed: formatUnits(effectivePriceReversed.amount, effectivePriceReversed.token.decimals), + routes: this.mapRoutes(paths, pools), + priceImpact: priceImpact, + }; + } + + private mapSwapTypeToSwapKind(swapType: GqlSorSwapType): SwapKind { + return swapType === 'EXACT_IN' ? SwapKind.GivenIn : SwapKind.GivenOut; + } + + private mapSwaps(paths: PathWithAmount[], swapKind: SwapKind): GqlSorSwap[] { + const swaps = this.getSwaps(paths, swapKind); + const assets = [...new Set(paths.flatMap((p) => p.tokens).map((t) => t.address))]; + + if (Array.isArray(swaps)) { + return swaps.map((swap) => { + return { + ...swap, + assetInIndex: Number(swap.assetInIndex.toString()), + assetOutIndex: Number(swap.assetOutIndex.toString()), + amount: swap.amount.toString(), + }; + }); + } else { + const assetInIndex = assets.indexOf(swaps.assetIn); + const assetOutIndex = assets.indexOf(swaps.assetOut); + return [ + { + ...swaps, + assetInIndex, + assetOutIndex, + amount: swaps.amount.toString(), + userData: swaps.userData, + }, + ]; + } + } + + private getSwaps(paths: PathWithAmount[], swapKind: SwapKind) { + const isBatchSwap = paths.length > 1 || paths[0].pools.length > 1; + const assets = [...new Set(paths.flatMap((p) => p.tokens).map((t) => t.address))]; + + let swaps: BatchSwapStep[] | SingleSwap; + if (isBatchSwap) { + swaps = [] as BatchSwapStep[]; + if (swapKind === SwapKind.GivenIn) { + paths.map((p) => { + p.pools.map((pool, i) => { + (swaps as BatchSwapStep[]).push({ + poolId: pool.id, + assetInIndex: BigInt(assets.indexOf(p.tokens[i].address)), + assetOutIndex: BigInt(assets.indexOf(p.tokens[i + 1].address)), + amount: i === 0 ? p.inputAmount.amount : 0n, + userData: DEFAULT_USERDATA, + }); + }); + }); + } else { + paths.map((p) => { + // Vault expects given out swaps to be in reverse order + const reversedPools = [...p.pools].reverse(); + const reversedTokens = [...p.tokens].reverse(); + reversedPools.map((pool, i) => { + (swaps as BatchSwapStep[]).push({ + poolId: pool.id, + assetInIndex: BigInt(assets.indexOf(reversedTokens[i + 1].address)), + assetOutIndex: BigInt(assets.indexOf(reversedTokens[i].address)), + amount: i === 0 ? p.outputAmount.amount : 0n, + userData: DEFAULT_USERDATA, + }); + }); + }); + } + } else { + const path = paths[0]; + const pool = path.pools[0]; + swaps = { + poolId: pool.id, + kind: swapKind, + assetIn: path.tokens[0].address, + assetOut: path.tokens[1].address, + amount: path.swapAmount.amount, + userData: DEFAULT_USERDATA, + } as SingleSwap; + } + return swaps; + } + + /** + * Fetch pools from Prisma and map to b-sdk BasePool. + * @returns + */ + private async getBasePoolsFromDb(chain: Chain): Promise { + const poolIdsToExclude = AllNetworkConfigsKeyedOnChain[chain].data.sor?.poolIdsToExclude ?? []; + const pools = await prisma.prismaPool.findMany({ + where: { + chain, + dynamicData: { + totalSharesNum: { + gt: 0.000000000001, + }, + swapEnabled: true, + totalLiquidity: { + gt: 1000, + }, + }, + id: { + notIn: [...poolIdsToExclude, ...poolsToIgnore], + }, + type: { + in: [ + 'WEIGHTED', + 'META_STABLE', + 'PHANTOM_STABLE', + 'COMPOSABLE_STABLE', + 'FX', + 'GYRO', + 'GYRO3', + 'GYROE', + ], + }, + }, + include: prismaPoolWithDynamic.include, + }); + return pools; + } + + private mapRoutes(paths: PathWithAmount[], pools: GqlPoolMinimal[]): GqlSorSwapRoute[] { + const isBatchSwap = paths.length > 1 || paths[0].pools.length > 1; + + if (!isBatchSwap) { + const pool = pools.find((p) => p.id === paths[0].pools[0].id); + if (!pool) throw new Error('Pool not found while mapping route'); + return [this.mapSingleSwap(paths[0], pool)]; + } + return paths.map((path) => this.mapBatchSwap(path, pools)); + } + + private mapBatchSwap(path: PathWithAmount, pools: GqlPoolMinimal[]): GqlSorSwapRoute { + const tokenIn = path.tokens[0].address; + const tokenOut = path.tokens[path.tokens.length - 1].address; + const tokenInAmount = formatUnits(path.inputAmount.amount, path.tokens[0].decimals); + const tokenOutAmount = formatUnits(path.inputAmount.amount, path.tokens[path.tokens.length - 1].decimals); + + return { + tokenIn, + tokenOut, + tokenInAmount, + tokenOutAmount, + share: 0, // TODO needed? + hops: path.pools.map((pool, i) => { + return { + tokenIn: `${path.tokens[i].address}`, + tokenOut: `${path.tokens[i + 1]}`, + tokenInAmount: i === 0 ? tokenInAmount : '0', + tokenOutAmount: i === pools.length - 1 ? tokenOutAmount : '0', + poolId: pool.id, + pool: pools.find((p) => p.id === pool.id) as GqlPoolMinimal, + }; + }), + }; + } + + private mapSingleSwap(path: PathWithAmount, pool: GqlPoolMinimal): GqlSorSwapRoute { + const tokenIn = path.tokens[0].address; + const tokenInAmount = formatUnits(path.inputAmount.amount, path.tokens[0].decimals); + const tokenOut = path.tokens[1].address; + const tokenOutAmount = formatUnits(path.inputAmount.amount, path.tokens[1].decimals); + + const hop: GqlSorSwapRouteHop = { + pool, + poolId: pool.id, + tokenIn, + tokenInAmount, + tokenOut, + tokenOutAmount, + }; + return { + share: 1, + tokenIn, + tokenOut, + tokenInAmount, + tokenOutAmount, + hops: [hop], + } as GqlSorSwapRoute; + } +} + +export const sorV2Service = new SorPathService(); diff --git a/modules/sor/sorV2/sorV2.service.ts b/modules/sor/sorV2/sorV2.service.ts deleted file mode 100644 index 425b024b9..000000000 --- a/modules/sor/sorV2/sorV2.service.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { GqlSorGetSwapPaths, GqlSorSwap, GqlSorSwapType } from '../../../schema'; -import { Chain } from '@prisma/client'; -import { PrismaPoolWithDynamic, prismaPoolWithDynamic } from '../../../prisma/prisma-types'; -import { prisma } from '../../../prisma/prisma-client'; -import { GetSwapsInput, GetSwapsV2Input as GetSwapPathsInput, SwapResult, SwapService } from '../types'; -import { env } from '../../../app/env'; -import { DeploymentEnv } from '../../network/network-config-types'; -import { poolsToIgnore } from '../constants'; -import { AllNetworkConfigsKeyedOnChain } from '../../network/network-config'; -import * as Sentry from '@sentry/node'; -import { Address, formatUnits, parseUnits } from 'viem'; -import { sorGetSwapsWithPools } from './lib/static'; -import { SwapResultV2 } from './swapResultV2'; -import { poolService } from '../../pool/pool.service'; -import { mapPaths, mapRoutes } from './beetsHelpers'; -import { replaceZeroAddressWithEth } from '../../web3/addresses'; -import { getToken, swapPathsZeroResponse } from '../utils'; -import { BatchSwapStep, SingleSwap, Swap, SwapKind, TokenAmount } from '@balancer/sdk'; -import { Token } from 'graphql'; - -export class SorV2Service implements SwapService { - public async getSwapResult( - { chain, tokenIn, tokenOut, swapType, swapAmount, graphTraversalConfig }: GetSwapsInput, - maxNonBoostedPathDepth = 4, - ): Promise { - try { - const poolsFromDb = await this.getBasePoolsFromDb(chain); - const tIn = await getToken(tokenIn as Address, chain); - const tOut = await getToken(tokenOut as Address, chain); - const swapKind = this.mapSwapTypeToSwapKind(swapType); - const config = graphTraversalConfig - ? { - graphTraversalConfig: { - maxNonBoostedPathDepth, - ...graphTraversalConfig, - }, - } - : { - graphTraversalConfig: { - maxNonBoostedPathDepth, - }, - }; - const swap = await sorGetSwapsWithPools(tIn, tOut, swapKind, swapAmount.amount, poolsFromDb, config); - if (!swap && maxNonBoostedPathDepth < 5) { - return this.getSwapResult(arguments[0], maxNonBoostedPathDepth + 1); - } - return new SwapResultV2(swap, chain); - } catch (err: any) { - if (err.message.includes('No potential swap paths provided') && maxNonBoostedPathDepth < 5) { - 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}`, - ); - Sentry.captureException(err.message, { - tags: { - service: 'sorV2', - tokenIn, - tokenOut, - swapAmount: swapAmount.amount, - swapType, - chain, - }, - }); - return new SwapResultV2(null, chain); - } - } - - public async getSorSwapPaths(input: GetSwapPathsInput, maxNonBoostedPathDepth = 4): Promise { - const swap = await this.getSwap(input, maxNonBoostedPathDepth); - const emptyResponse = swapPathsZeroResponse(input.tokenIn, input.tokenOut); - - if (!swap) { - return emptyResponse; - } - - try { - return this.mapToSorSwapPaths(swap!, input.chain, input.queryBatchSwap); - } catch (err: any) { - console.log(`Error Retrieving QuerySwap`, err); - Sentry.captureException(err.message, { - tags: { - service: 'sorV2 query swap', - tokenIn: input.tokenIn, - tokenOut: input.tokenOut, - swapAmount: formatUnits(input.swapAmount.amount, input.swapAmount.token.decimals), - swapType: input.swapType, - chain: input.chain, - }, - }); - return emptyResponse; - } - } - - private async getSwap( - { chain, tokenIn, tokenOut, swapType, swapAmount, graphTraversalConfig }: GetSwapPathsInput, - maxNonBoostedPathDepth = 4, - ): Promise { - try { - const poolsFromDb = await this.getBasePoolsFromDb(chain); - const tIn = await getToken(tokenIn as Address, chain); - const tOut = await getToken(tokenOut as Address, chain); - const swapKind = this.mapSwapTypeToSwapKind(swapType); - const config = graphTraversalConfig - ? { - graphTraversalConfig: { - maxNonBoostedPathDepth, - ...graphTraversalConfig, - }, - } - : { - graphTraversalConfig: { - maxNonBoostedPathDepth, - }, - }; - const swap = await sorGetSwapsWithPools(tIn, tOut, swapKind, swapAmount.amount, poolsFromDb, config); - // if we dont find a path with depth 4, we try one more level. - if (!swap && maxNonBoostedPathDepth < 5) { - return this.getSwap(arguments[0], maxNonBoostedPathDepth + 1); - } - return swap; - } catch (err: any) { - console.error( - `SOR_V2_ERROR ${err.message} - tokenIn: ${tokenIn} - tokenOut: ${tokenOut} - swapAmount: ${swapAmount.amount} - swapType: ${swapType} - chain: ${chain}`, - ); - Sentry.captureException(err.message, { - tags: { - service: 'sorV2', - tokenIn, - tokenOut, - swapAmount: swapAmount.amount, - swapType, - chain, - }, - }); - return null; - } - } - - public async mapToSorSwapPaths(swap: Swap, chain: Chain, queryFirst = false): Promise { - if (!queryFirst) return this.mapSwapToSorGetSwaps(swap, swap.inputAmount, swap.outputAmount); - else { - const rpcUrl = AllNetworkConfigsKeyedOnChain[chain].data.rpcUrl; - const updatedResult = await swap.query(rpcUrl); - - const inputAmount = swap.swapKind === SwapKind.GivenIn ? swap.inputAmount : updatedResult; - const outputAmount = swap.swapKind === SwapKind.GivenIn ? updatedResult : swap.outputAmount; - - return this.mapSwapToSorGetSwaps(swap, inputAmount, outputAmount); - } - } - - private async mapSwapToSorGetSwaps( - swap: Swap, - inputAmount: TokenAmount, - outputAmount: TokenAmount, - ): Promise { - const priceImpact = swap.priceImpact.decimal.toFixed(4); - let poolIds: string[]; - if (swap.isBatchSwap) { - const swaps = swap.swaps as BatchSwapStep[]; - poolIds = swaps.map((swap) => swap.poolId); - } else { - const singleSwap = swap.swaps as SingleSwap; - poolIds = [singleSwap.poolId]; - } - const pools = await poolService.getGqlPools({ - where: { idIn: poolIds }, - }); - - const returnAmount = swap.swapKind === SwapKind.GivenIn ? outputAmount : inputAmount; - const swapAmount = swap.swapKind === SwapKind.GivenIn ? inputAmount : outputAmount; - - const effectivePrice = inputAmount.divDownFixed(outputAmount.amount); - const effectivePriceReversed = outputAmount.divDownFixed(inputAmount.amount); - - console.log(effectivePrice.amount); - console.log(effectivePriceReversed.amount); - - const routes = mapRoutes( - swap.swaps, - inputAmount.amount.toString(), - outputAmount.amount.toString(), - pools, - swap.inputAmount.token.address, - swap.outputAmount.token.address, - swap.assets, - swap.swapKind, - ); - - const paths = mapPaths(swap); - - for (const route of routes) { - route.tokenInAmount = ((inputAmount.amount * BigInt(parseUnits(`${0.5}`, 6))) / 1000000n).toString(); - route.tokenOutAmount = ( - (outputAmount.amount * BigInt(parseUnits(`${route.share}`, 6))) / - 1000000n - ).toString(); - } - - return { - vaultVersion: 2, - paths: paths, - swapType: this.mapSwapKindToSwapType(swap.swapKind), - swaps: this.mapSwaps(swap.swaps, swap.assets), - tokenIn: replaceZeroAddressWithEth(inputAmount.token.address), - tokenOut: replaceZeroAddressWithEth(outputAmount.token.address), - tokenInAmount: inputAmount.amount.toString(), - tokenOutAmount: outputAmount.amount.toString(), - swapAmount: formatUnits(swapAmount.amount, swapAmount.token.decimals), - swapAmountScaled: swapAmount.amount.toString(), - returnAmount: formatUnits(returnAmount.amount, returnAmount.token.decimals), - returnAmountScaled: returnAmount.amount.toString(), - effectivePrice: formatUnits(effectivePrice.amount, effectivePrice.token.decimals), - effectivePriceReversed: formatUnits(effectivePriceReversed.amount, effectivePriceReversed.token.decimals), - routes: routes.map((route) => ({ - ...route, - hops: route.hops.map((hop) => ({ - ...hop, - pool: pools.find((pool) => pool.id === hop.poolId)!, - })), - })), - priceImpact: priceImpact, - }; - } - - private mapSwapTypeToSwapKind(swapType: GqlSorSwapType): SwapKind { - return swapType === 'EXACT_IN' ? SwapKind.GivenIn : SwapKind.GivenOut; - } - - private mapSwapKindToSwapType(swapKind: SwapKind): GqlSorSwapType { - return swapKind === SwapKind.GivenIn ? 'EXACT_IN' : 'EXACT_OUT'; - } - - private mapSwaps(swaps: BatchSwapStep[] | SingleSwap, assets: string[]): GqlSorSwap[] { - if (Array.isArray(swaps)) { - return swaps.map((swap) => { - return { - ...swap, - assetInIndex: Number(swap.assetInIndex.toString()), - assetOutIndex: Number(swap.assetOutIndex.toString()), - amount: swap.amount.toString(), - }; - }); - } else { - const assetInIndex = assets.indexOf(swaps.assetIn); - const assetOutIndex = assets.indexOf(swaps.assetOut); - return [ - { - ...swaps, - assetInIndex, - assetOutIndex, - amount: swaps.amount.toString(), - userData: swaps.userData, - }, - ]; - } - } - - /** - * Fetch pools from Prisma and map to b-sdk BasePool. - * @returns - */ - private async getBasePoolsFromDb(chain: Chain): Promise { - const poolIdsToExclude = AllNetworkConfigsKeyedOnChain[chain].data.sor?.poolIdsToExclude ?? []; - const pools = await prisma.prismaPool.findMany({ - where: { - chain, - dynamicData: { - totalSharesNum: { - gt: 0.000000000001, - }, - swapEnabled: true, - totalLiquidity: { - gt: 1000, - }, - }, - id: { - notIn: [...poolIdsToExclude, ...poolsToIgnore], - }, - type: { - in: [ - 'WEIGHTED', - 'META_STABLE', - 'PHANTOM_STABLE', - 'COMPOSABLE_STABLE', - 'FX', - 'GYRO', - 'GYRO3', - 'GYROE', - ], - }, - }, - include: prismaPoolWithDynamic.include, - }); - return pools; - } -} - -export const sorV2Service = new SorV2Service(); diff --git a/modules/sor/sorV2/swapResultV2.ts b/modules/sor/sorV2/swapResultV2.ts index 284f65a09..d6087c6f3 100644 --- a/modules/sor/sorV2/swapResultV2.ts +++ b/modules/sor/sorV2/swapResultV2.ts @@ -1,24 +1,30 @@ -import { GqlSorSwapType, GqlSorGetSwapsResponse, GqlSorSwap } from '../../../schema'; +import { + GqlSorSwapType, + GqlSorGetSwapsResponse, + GqlSorSwap, + GqlPoolMinimal, + GqlSorSwapRoute, + GqlSorSwapRouteHop, +} from '../../../schema'; import { Chain } from '@prisma/client'; import { SwapResult } from '../types'; import { poolService } from '../../pool/pool.service'; import { BigNumber } from 'ethers'; import { oldBnum } from '../../big-number/old-big-number'; -import { mapRoutes } from './beetsHelpers'; import { AllNetworkConfigsKeyedOnChain } from '../../network/network-config'; -import { Address } from 'viem'; import { formatFixed } from '@ethersproject/bignumber'; import { replaceZeroAddressWithEth } from '../../web3/addresses'; -import { BatchSwapStep, SingleSwap, Swap, SwapKind, TokenAmount } from '@balancer/sdk'; +import { BatchSwapStep, SingleSwap, SwapKind, TokenAmount, ZERO_ADDRESS } from '@balancer/sdk'; +import { SwapLocal } from './lib/swapLocal'; export class SwapResultV2 implements SwapResult { - private swap: Swap | null; + private swap: SwapLocal | null; private chain: Chain; public inputAmount: bigint = BigInt(0); public outputAmount: bigint = BigInt(0); public isValid: boolean; - constructor(swap: Swap | null, chain: Chain) { + constructor(swap: SwapLocal | null, chain: Chain) { if (swap === null) { this.isValid = false; this.swap = null; @@ -48,7 +54,7 @@ export class SwapResultV2 implements SwapResult { } private async mapResultToBeetsSwap( - swap: Swap, + swap: SwapLocal, inputAmount: TokenAmount, outputAmount: TokenAmount, ): Promise { @@ -83,7 +89,7 @@ export class SwapResultV2 implements SwapResult { const effectivePrice = oldBnum(tokenInAmountFixed).div(tokenOutAmountFixed); const effectivePriceReversed = oldBnum(tokenOutAmountFixed).div(tokenInAmountFixed); - const routes = mapRoutes( + const routes = this.mapRoutes( swap.swaps, inputAmount.amount.toString(), outputAmount.amount.toString(), @@ -163,6 +169,132 @@ export class SwapResultV2 implements SwapResult { return kind === SwapKind.GivenIn ? 'EXACT_IN' : 'EXACT_OUT'; } + private mapRoutes( + swaps: BatchSwapStep[] | SingleSwap, + amountIn: string, + amountOut: string, + pools: GqlPoolMinimal[], + assetIn: string, + assetOut: string, + assets: string[], + kind: SwapKind, + ): GqlSorSwapRoute[] { + const isBatchSwap = Array.isArray(swaps); + if (!isBatchSwap) { + const pool = pools.find((p) => p.id === swaps.poolId); + if (!pool) throw new Error('Pool not found while mapping route'); + return [this.mapSingleSwap(swaps, amountIn, amountOut, pool)]; + } + const paths = this.splitPaths(swaps, assetIn, assetOut, assets, kind); + return paths.map((p) => this.mapBatchSwap(p, amountIn, amountOut, kind, assets, pools)); + } + + private mapBatchSwap( + swaps: BatchSwapStep[], + amountIn: string, + amountOut: string, + kind: SwapKind, + assets: string[], + pools: GqlPoolMinimal[], + ): GqlSorSwapRoute { + const exactIn = kind === SwapKind.GivenIn; + const first = swaps[0]; + const last = swaps[swaps.length - 1]; + let tokenInAmount: string; + let tokenOutAmount: string; + let share: bigint; + const one = BigInt(1e18); + if (exactIn) { + tokenInAmount = first.amount.toString(); + share = (BigInt(tokenInAmount) * one) / BigInt(amountIn); + tokenOutAmount = ((BigInt(amountOut) * share) / one).toString(); + } else { + tokenOutAmount = last.amount.toString(); + share = (BigInt(tokenOutAmount) * one) / BigInt(amountOut); + tokenInAmount = ((BigInt(amountIn) * share) / one).toString(); + } + + return { + tokenIn: assets[Number(first.assetInIndex)], + tokenOut: assets[Number(last.assetOutIndex)], + tokenInAmount, + tokenOutAmount, + share: Number(formatFixed(share.toString(), 18)), + hops: swaps.map((swap, i) => { + return { + tokenIn: assets[Number(swap.assetInIndex)], + tokenOut: assets[Number(swap.assetOutIndex)], + tokenInAmount: i === 0 ? tokenInAmount : '0', + tokenOutAmount: i === swaps.length - 1 ? tokenOutAmount : '0', + poolId: swap.poolId, + pool: pools.find((p) => p.id === swap.poolId) as GqlPoolMinimal, + }; + }), + }; + } + + private splitPaths( + swaps: BatchSwapStep[], + assetIn: string, + assetOut: string, + assets: string[], + kind: SwapKind, + ): BatchSwapStep[][] { + const swapsCopy = [...swaps]; + if (kind === SwapKind.GivenOut) { + swapsCopy.reverse(); + } + const assetInIndex = BigInt( + assets.indexOf( + assetIn === AllNetworkConfigsKeyedOnChain[this.chain].data.eth.address ? ZERO_ADDRESS : assetIn, + ), + ); + const assetOutIndex = BigInt( + assets.indexOf( + assetOut === AllNetworkConfigsKeyedOnChain[this.chain].data.eth.address ? ZERO_ADDRESS : assetOut, + ), + ); + let path: BatchSwapStep[]; + let paths: BatchSwapStep[][] = []; + swapsCopy.forEach((swap) => { + if (swap.assetInIndex === assetInIndex && swap.assetOutIndex === assetOutIndex) { + paths.push([swap]); + } else if (swap.assetInIndex === assetInIndex) { + path = [swap]; + } else if (swap.assetOutIndex === assetOutIndex) { + path.push(swap); + paths.push(path); + } else { + path.push(swap); + } + }); + return paths; + } + + private mapSingleSwap( + swap: SingleSwap, + amountIn: string, + amountOut: string, + pool: GqlPoolMinimal, + ): GqlSorSwapRoute { + const hop: GqlSorSwapRouteHop = { + pool, + poolId: swap.poolId, + tokenIn: swap.assetIn, + tokenInAmount: amountIn, + tokenOut: swap.assetOut, + tokenOutAmount: amountOut, + }; + return { + share: 1, + tokenIn: swap.assetIn, + tokenOut: swap.assetOut, + tokenInAmount: amountIn, + tokenOutAmount: amountOut, + hops: [hop], + } as GqlSorSwapRoute; + } + // /** // * Formats a sequence of swaps to a format that is useful for displaying the routes in user interfaces. // * Taken directly from Beets SOR: https://github.com/beethovenxfi/balancer-sor/blob/beethovenx-master/src/formatSwaps.ts#L167 diff --git a/modules/sor/utils.ts b/modules/sor/utils.ts index a9e784960..935c0e2c7 100644 --- a/modules/sor/utils.ts +++ b/modules/sor/utils.ts @@ -4,7 +4,7 @@ import { AllNetworkConfigsKeyedOnChain, chainToIdMap } from '../network/network- import { GqlSorGetSwapPaths, GqlSorGetSwapsResponse, GqlSorSwapType } from '../../schema'; import { replaceZeroAddressWithEth } from '../web3/addresses'; import { Address } from 'viem'; -import { NATIVE_ADDRESS, Token, TokenAmount } from '@balancer/sdk'; +import { Token, TokenAmount } from '@balancer/sdk'; export async function getTokenAmountHuman(tokenAddr: string, humanAmount: string, chain: Chain): Promise { const token = await getToken(tokenAddr, chain); @@ -23,8 +23,7 @@ export async function getTokenAmountRaw(tokenAddr: string, rawAmount: string, ch * @returns */ export const getToken = async (tokenAddr: string, chain: Chain): Promise => { - // also check for the polygon native asset - if (tokenAddr === NATIVE_ADDRESS || tokenAddr === '0x0000000000000000000000000000000000001010') { + if (tokenAddr === AllNetworkConfigsKeyedOnChain[chain].data.eth.address) { return new Token( parseFloat(chainToIdMap[chain]), AllNetworkConfigsKeyedOnChain[chain].data.weth.address as Address, @@ -41,6 +40,7 @@ export const swapPathsZeroResponse = (tokenIn: string, tokenOut: string): GqlSor return { swaps: [], paths: [], + tokenAddresses: [], swapType: 'EXACT_IN', vaultVersion: 2, tokenIn: replaceZeroAddressWithEth(tokenIn), @@ -48,9 +48,9 @@ export const swapPathsZeroResponse = (tokenIn: string, tokenOut: string): GqlSor tokenInAmount: '0', tokenOutAmount: '0', swapAmount: '0', - swapAmountScaled: '0', + swapAmountRaw: '0', returnAmount: '0', - returnAmountScaled: '0', + returnAmountRaw: '0', effectivePrice: '0', effectivePriceReversed: '0', routes: [], diff --git a/package.json b/package.json index 98b667b9e..43d6c0240 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "@aws-sdk/client-secrets-manager": "^3.195.0", "@aws-sdk/client-sqs": "^3.137.0", "@balancer-labs/sdk": "github:beethovenxfi/balancer-sdk#beethovenx-master", - "@balancer/sdk": "^0.6.0", + "@balancer/sdk": "^0.11.0", "@ethersproject/address": "^5.6.0", "@ethersproject/bignumber": "^5.6.0", "@ethersproject/constants": "^5.6.0", diff --git a/yarn.lock b/yarn.lock index 5ae87b4a2..ea02eadec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2456,16 +2456,14 @@ graphology "^0.24.1" isomorphic-fetch "^2.2.1" -"@balancer/sdk@^0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@balancer/sdk/-/sdk-0.6.0.tgz#9b842516a3d7c1ccea06492e52bcd71d7f2e9d6c" - integrity sha512-Yp6vJteGGqx7MvrrobClKB2eYKg5kXJhidz0JmbsYe6zUHhQJmy61iBbBZR7+qSzsquz0HWe26HK0VC0/kFW/g== +"@balancer/sdk@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@balancer/sdk/-/sdk-0.11.0.tgz#42062328bf7277553fbb476a25b662db4c1890fe" + integrity sha512-L1bHra1euk4amRCqdtfKs4AX4IXDHWGIpqupQAnjo4ce7fLWMHKeruy/DaCiP+sGMf3wIsAn6AP3USq1QPnfgA== dependencies: - async-retry "^1.3.3" decimal.js-light "^2.5.1" - lodash "^4.17.21" - pino "^8.11.0" - viem "^1.9.3" + lodash.clonedeep "^4.5.0" + viem "^2.1.1" "@bcoe/v8-coverage@^0.2.3": version "0.2.3" @@ -5079,11 +5077,6 @@ abbrev@1, abbrev@^1.0.0: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -abitype@0.9.8: - version "0.9.8" - resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.9.8.tgz#1f120b6b717459deafd213dfbf3a3dd1bf10ae8c" - integrity sha512-puLifILdm+8sjyss4S+fsUN09obiT1g2YW6CtcQF+QDzxR0euzgEB29MZujC6zMk2a6SVmtttq1fc6+YFA7WYQ== - abitype@1.0.0, abitype@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.0.tgz#237176dace81d90d018bebf3a45cb42f2a2d9e97" @@ -5416,7 +5409,7 @@ assertion-error@^1.1.0: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== -async-retry@^1.2.1, async-retry@^1.3.3: +async-retry@^1.2.1: version "1.3.3" resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== @@ -5804,14 +5797,6 @@ buffer@^5.5.0, buffer@^5.7.0: base64-js "^1.3.1" ieee754 "^1.1.13" -buffer@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - byline@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" @@ -7174,11 +7159,6 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-sta resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-redact@^3.1.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.3.0.tgz#7c83ce3a7be4898241a46560d51de10f653f7634" - integrity sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ== - fast-text-encoding@^1.0.0: version "1.0.6" resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" @@ -7959,7 +7939,7 @@ iconv-lite@^0.6.2: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -ieee754@^1.1.13, ieee754@^1.2.1: +ieee754@^1.1.13: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -9215,6 +9195,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== + lodash.deburr@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/lodash.deburr/-/lodash.deburr-4.1.0.tgz#ddb1bbb3ef07458c0177ba07de14422cb033ff9b" @@ -9967,11 +9952,6 @@ obliterator@^2.0.2: resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.4.tgz#fa650e019b2d075d745e44f1effeb13a2adbe816" integrity sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ== -on-exit-leak-free@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8" - integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA== - on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -10240,36 +10220,6 @@ picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -pino-abstract-transport@v1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz#083d98f966262164504afb989bccd05f665937a8" - integrity sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA== - dependencies: - readable-stream "^4.0.0" - split2 "^4.0.0" - -pino-std-serializers@^6.0.0: - version "6.2.2" - resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz#d9a9b5f2b9a402486a5fc4db0a737570a860aab3" - integrity sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA== - -pino@^8.11.0: - version "8.18.0" - resolved "https://registry.yarnpkg.com/pino/-/pino-8.18.0.tgz#f2bfbb4e827ed2049ee1e88372268efdcd1505f6" - integrity sha512-Mz/gKiRyuXu4HnpHgi1YWdHQCoWMufapzooisvFn78zl4dZciAxS+YeRkUxXl1ee/SzU80YCz1zpECCh4oC6Aw== - dependencies: - atomic-sleep "^1.0.0" - fast-redact "^3.1.1" - on-exit-leak-free "^2.1.0" - pino-abstract-transport v1.1.0 - pino-std-serializers "^6.0.0" - process-warning "^3.0.0" - quick-format-unescaped "^4.0.3" - real-require "^0.2.0" - safe-stable-stringify "^2.3.1" - sonic-boom "^3.7.0" - thread-stream "^2.0.0" - pirates@^4.0.4: version "4.0.5" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" @@ -10356,16 +10306,6 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -process-warning@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-3.0.0.tgz#96e5b88884187a1dce6f5c3166d611132058710b" - integrity sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ== - -process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== - progress-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/progress-stream/-/progress-stream-2.0.0.tgz#fac63a0b3d11deacbb0969abcc93b214bce19ed5" @@ -10459,11 +10399,6 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -quick-format-unescaped@^4.0.3: - version "4.0.4" - resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" - integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== - random-words@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/random-words/-/random-words-1.3.0.tgz#00715efb8dd787d244f963c994367707c1e95676" @@ -10535,17 +10470,6 @@ readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^4.0.0: - version "4.5.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" - integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== - dependencies: - abort-controller "^3.0.0" - buffer "^6.0.3" - events "^3.3.0" - process "^0.11.10" - string_decoder "^1.3.0" - readdir-glob@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.1.tgz#f0e10bb7bf7bfa7e0add8baffdc54c3f7dbee6c4" @@ -10560,11 +10484,6 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -real-require@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" - integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== - regenerator-runtime@^0.13.4: version "0.13.9" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" @@ -10809,11 +10728,6 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" -safe-stable-stringify@^2.3.1: - version "2.4.3" - resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" - integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== - "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -11041,13 +10955,6 @@ sonic-boom@^2.1.0: dependencies: atomic-sleep "^1.0.0" -sonic-boom@^3.7.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.8.0.tgz#e442c5c23165df897d77c3c14ef3ca40dec66a66" - integrity sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA== - dependencies: - atomic-sleep "^1.0.0" - source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" @@ -11094,11 +11001,6 @@ split-ca@^1.0.1: resolved "https://registry.yarnpkg.com/split-ca/-/split-ca-1.0.1.tgz#6c83aff3692fa61256e0cd197e05e9de157691a6" integrity sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY= -split2@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" - integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== - sponge-case@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/sponge-case/-/sponge-case-1.0.1.tgz#260833b86453883d974f84854cdb63aecc5aef4c" @@ -11251,7 +11153,7 @@ string.prototype.trimstart@^1.0.5: define-properties "^1.1.4" es-abstract "^1.19.5" -string_decoder@^1.1.1, string_decoder@^1.3.0: +string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -11474,13 +11376,6 @@ testcontainers@^8.0.0: ssh-remote-port-forward "^1.0.4" tar-fs "^2.1.1" -thread-stream@^2.0.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.4.1.tgz#6d588b14f0546e59d3f306614f044bc01ce43351" - integrity sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg== - dependencies: - real-require "^0.2.0" - through2@~2.0.3: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" @@ -11893,20 +11788,6 @@ vary@^1, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= -viem@^1.9.3: - version "1.21.4" - resolved "https://registry.yarnpkg.com/viem/-/viem-1.21.4.tgz#883760e9222540a5a7e0339809202b45fe6a842d" - integrity sha512-BNVYdSaUjeS2zKQgPs+49e5JKocfo60Ib2yiXOWBT6LuVxY1I/6fFX3waEtpXvL1Xn4qu+BVitVtMh9lyThyhQ== - dependencies: - "@adraffy/ens-normalize" "1.10.0" - "@noble/curves" "1.2.0" - "@noble/hashes" "1.3.2" - "@scure/bip32" "1.3.2" - "@scure/bip39" "1.2.1" - abitype "0.9.8" - isows "1.0.3" - ws "8.13.0" - viem@^2.1.1: version "2.5.0" resolved "https://registry.yarnpkg.com/viem/-/viem-2.5.0.tgz#1d7bd5333a6b9387d42c1c2d368d0b88c2961ee1"