diff --git a/balancer-js/examples/swaps/swap.ts b/balancer-js/examples/swaps/swap.ts index 98924d382..44c27aa86 100644 --- a/balancer-js/examples/swaps/swap.ts +++ b/balancer-js/examples/swaps/swap.ts @@ -1,72 +1,60 @@ /** * How to build a swap and send it using ethers.js - * + * * How to run: * yarn example examples/swaps/swap.ts */ -import { BalancerSDK, Network } from '@balancer-labs/sdk' -import { formatFixed } from '@ethersproject/bignumber' -import { AddressZero } from '@ethersproject/constants' +import { BalancerSDK, Network } from '@balancer-labs/sdk'; +import { formatFixed } from '@ethersproject/bignumber'; +import { AddressZero } from '@ethersproject/constants'; -const tokenIn = AddressZero // eth -const tokenOut = '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599' // wBTC -const amount = String(BigInt(100e18)) // 100 eth +const tokenIn = AddressZero; // eth +const tokenOut = '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599'; // wBTC +const amount = String(BigInt(100e18)); // 100 eth const sdk = new BalancerSDK({ network: Network.MAINNET, rpcUrl: `http://127.0.0.1:8545`, // Uses a local fork for simulating transaction sending. -}) +}); -const { swaps } = sdk +const { swaps } = sdk; -const erc20Out = sdk.contracts.ERC20(tokenOut, sdk.provider) +const erc20Out = sdk.contracts.ERC20(tokenOut, sdk.provider); async function swap() { - const signer = sdk.provider.getSigner() - const account = await signer.getAddress() + const signer = sdk.provider.getSigner(); + const account = await signer.getAddress(); // Finding a trading route rely on on-chain data. // fetchPools will fetch the current data from the subgraph. // Let's fetch just 5 pools with highest liquidity of tokenOut. - await swaps.fetchPools({ - first: 5, - where: { - swapEnabled: { - eq: true, - }, - tokensList: { - contains: [tokenOut], - }, - }, - orderBy: 'totalLiquidity', - orderDirection: 'desc', - }) + await swaps.fetchPools(undefined, 200); // Set exectution deadline to 60 seconds from now - const deadline = String(Math.ceil(Date.now() / 1000) + 60) + const deadline = String(Math.ceil(Date.now() / 1000) + 60); // Avoid getting rekt by setting low slippage from expected amounts out, 10 bsp = 0.1% - const maxSlippage = 10 + const maxSlippage = 10; // Building the route payload const payload = await swaps.buildRouteExactIn( account, account, - tokenIn, // eth + tokenIn, // eth tokenOut, // wBTC amount, { maxSlippage, - deadline + deadline, } - ) + ); // Extract parameters required for sendTransaction - const { to, data, value } = payload + const { to, data, value } = payload; // Execution with ethers.js try { - const balanceBefore = await erc20Out.balanceOf(account) + const balanceBefore = await erc20Out.balanceOf(account); await ( await signer.sendTransaction({ @@ -74,20 +62,20 @@ async function swap() { data, value, }) - ).wait() + ).wait(); // check delta - const balanceAfter = await erc20Out.balanceOf(account) + const balanceAfter = await erc20Out.balanceOf(account); console.log( `Amount of BTC received: ${formatFixed( balanceAfter.sub(balanceBefore), 8 )}` - ) + ); } catch (err) { - console.log(err) + console.log(err); } } -swap() +swap(); diff --git a/balancer-js/src/modules/sor/pool-data/onChainData.ts b/balancer-js/src/modules/sor/pool-data/onChainData.ts index 97d5d423c..ac8f15c94 100644 --- a/balancer-js/src/modules/sor/pool-data/onChainData.ts +++ b/balancer-js/src/modules/sor/pool-data/onChainData.ts @@ -20,6 +20,8 @@ import { import { JsonFragment } from '@ethersproject/abi'; import { Multicaller } from '@/lib/utils/multiCaller'; import { isSameAddress } from '@/lib/utils'; +import _ from 'lodash'; +import { MulticallPool } from '@/modules/sor/pool-data/types'; export type Tokens = (SubgraphToken | PoolToken)[]; @@ -62,221 +64,24 @@ export async function getOnChainBalances< subgraphPoolsOriginal: GenericPool[], multiAddress: string, vaultAddress: string, - provider: Provider + provider: Provider, + chunkSize?: number ): Promise { if (subgraphPoolsOriginal.length === 0) return subgraphPoolsOriginal; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const abis: any = Object.values( - // Remove duplicate entries using their names - Object.fromEntries( - [ - ...(Vault__factory.abi as readonly JsonFragment[]), - ...(StaticATokenRateProvider__factory.abi as readonly JsonFragment[]), - ...(WeightedPool__factory.abi as readonly JsonFragment[]), - ...(StablePool__factory.abi as readonly JsonFragment[]), - ...(ConvergentCurvePool__factory.abi as readonly JsonFragment[]), - ...(LinearPool__factory.abi as readonly JsonFragment[]), - ...(ComposableStablePool__factory.abi as readonly JsonFragment[]), - ...(GyroEV2__factory.abi as readonly JsonFragment[]), - ].map((row) => [row.name, row]) - ) + const { multipools, poolsToBeCalled } = generateMultipools( + subgraphPoolsOriginal, + vaultAddress, + multiAddress, + provider, + chunkSize ); - const multicall = Multicall__factory.connect(multiAddress, provider); - - const multiPool = new Multicaller(multicall, abis); - - const supportedPoolTypes: string[] = Object.values(PoolType); - const subgraphPools: GenericPool[] = []; - subgraphPoolsOriginal.forEach((pool) => { - if ( - !supportedPoolTypes.includes(pool.poolType) || - pool.poolType === 'Managed' - ) { - const logger = Logger.getInstance(); - logger.warn(`Unknown pool type: ${pool.poolType} ${pool.id}`); - return; - } - - subgraphPools.push(pool); - - multiPool.call(`${pool.id}.poolTokens`, vaultAddress, 'getPoolTokens', [ - pool.id, - ]); - multiPool.call(`${pool.id}.totalSupply`, pool.address, 'totalSupply'); - - switch (pool.poolType) { - case 'LiquidityBootstrapping': - case 'Investment': - case 'Weighted': - multiPool.call( - `${pool.id}.swapFee`, - pool.address, - 'getSwapFeePercentage' - ); - multiPool.call( - `${pool.id}.weights`, - pool.address, - 'getNormalizedWeights' - ); - break; - case 'StablePhantom': - multiPool.call( - `${pool.id}.virtualSupply`, - pool.address, - 'getVirtualSupply' - ); - multiPool.call( - `${pool.id}.amp`, - pool.address, - 'getAmplificationParameter' - ); - multiPool.call( - `${pool.id}.swapFee`, - pool.address, - 'getSwapFeePercentage' - ); - break; - // MetaStable is the same as Stable for multicall purposes - case 'MetaStable': - case 'Stable': - multiPool.call( - `${pool.id}.amp`, - pool.address, - 'getAmplificationParameter' - ); - multiPool.call( - `${pool.id}.swapFee`, - pool.address, - 'getSwapFeePercentage' - ); - break; - case 'ComposableStable': - /** - * Returns the effective BPT supply. - * In other pools, this would be the same as `totalSupply`, but there are two key differences here: - * - this pool pre-mints BPT and holds it in the Vault as a token, and as such we need to subtract the Vault's - * balance to get the total "circulating supply". This is called the 'virtualSupply'. - * - the Pool owes debt to the Protocol in the form of unminted BPT, which will be minted immediately before the - * next join or exit. We need to take these into account since, even if they don't yet exist, they will - * effectively be included in any Pool operation that involves BPT. - * In the vast majority of cases, this function should be used instead of `totalSupply()`. - */ - multiPool.call( - `${pool.id}.actualSupply`, - pool.address, - 'getActualSupply' - ); - // MetaStable & StablePhantom is the same as Stable for multicall purposes - multiPool.call( - `${pool.id}.amp`, - pool.address, - 'getAmplificationParameter' - ); - multiPool.call( - `${pool.id}.swapFee`, - pool.address, - 'getSwapFeePercentage' - ); - break; - case 'Element': - multiPool.call(`${pool.id}.swapFee`, pool.address, 'percentFee'); - break; - case 'Gyro2': - case 'Gyro3': - multiPool.call(`${pool.id}.poolTokens`, vaultAddress, 'getPoolTokens', [ - pool.id, - ]); - multiPool.call(`${pool.id}.totalSupply`, pool.address, 'totalSupply'); - multiPool.call( - `${pool.id}.swapFee`, - pool.address, - 'getSwapFeePercentage' - ); - break; - case 'GyroE': - multiPool.call( - `${pool.id}.swapFee`, - pool.address, - 'getSwapFeePercentage' - ); - if (pool.poolTypeVersion && pool.poolTypeVersion === 2) { - multiPool.call( - `${pool.id}.tokenRates`, - pool.address, - 'getTokenRates' - ); - } - break; - default: - //Handling all Linear pools - if (pool.poolType.toString().includes('Linear')) { - multiPool.call( - `${pool.id}.virtualSupply`, - pool.address, - 'getVirtualSupply' - ); - multiPool.call( - `${pool.id}.swapFee`, - pool.address, - 'getSwapFeePercentage' - ); - multiPool.call(`${pool.id}.targets`, pool.address, 'getTargets'); - multiPool.call( - `${pool.id}.rate`, - pool.address, - 'getWrappedTokenRate' - ); - } - break; - } - }); - - let pools = {} as Record< - string, - { - amp?: string[]; - swapFee: string; - weights?: string[]; - targets?: string[]; - poolTokens: { - tokens: string[]; - balances: string[]; - }; - totalSupply: string; - virtualSupply?: string; - rate?: string; - actualSupply?: string; - tokenRates?: string[]; - } - >; - - try { - pools = (await multiPool.execute()) as Record< - string, - { - amp?: string[]; - swapFee: string; - weights?: string[]; - poolTokens: { - tokens: string[]; - balances: string[]; - }; - totalSupply: string; - virtualSupply?: string; - rate?: string; - actualSupply?: string; - tokenRates?: string[]; - } - >; - } catch (err) { - throw new Error(`Issue with multicall execution.`); - } + const multicallPools = await executeChunks(multipools); const onChainPools: GenericPool[] = []; - Object.entries(pools).forEach(([poolId, onchainData], index) => { + Object.entries(multicallPools).forEach(([poolId, onchainData], index) => { try { const { poolTokens, @@ -289,10 +94,10 @@ export async function getOnChainBalances< } = onchainData; if ( - subgraphPools[index].poolType === 'Stable' || - subgraphPools[index].poolType === 'MetaStable' || - subgraphPools[index].poolType === 'StablePhantom' || - subgraphPools[index].poolType === 'ComposableStable' + poolsToBeCalled[index].poolType === 'Stable' || + poolsToBeCalled[index].poolType === 'MetaStable' || + poolsToBeCalled[index].poolType === 'StablePhantom' || + poolsToBeCalled[index].poolType === 'ComposableStable' ) { if (!onchainData.amp) { console.error(`Stable Pool Missing Amp: ${poolId}`); @@ -300,26 +105,26 @@ export async function getOnChainBalances< } else { // Need to scale amp by precision to match expected Subgraph scale // amp is stored with 3 decimals of precision - subgraphPools[index].amp = formatFixed(onchainData.amp[0], 3); + poolsToBeCalled[index].amp = formatFixed(onchainData.amp[0], 3); } } - if (subgraphPools[index].poolType.includes('Linear')) { + if (poolsToBeCalled[index].poolType.includes('Linear')) { if (!onchainData.targets) { console.error(`Linear Pool Missing Targets: ${poolId}`); return; } else { - subgraphPools[index].lowerTarget = formatFixed( + poolsToBeCalled[index].lowerTarget = formatFixed( onchainData.targets[0], 18 ); - subgraphPools[index].upperTarget = formatFixed( + poolsToBeCalled[index].upperTarget = formatFixed( onchainData.targets[1], 18 ); } - const wrappedIndex = subgraphPools[index].wrappedIndex; + const wrappedIndex = poolsToBeCalled[index].wrappedIndex; if (wrappedIndex === undefined || onchainData.rate === undefined) { console.error( `Linear Pool Missing WrappedIndex or PriceRate: ${poolId}` @@ -327,17 +132,17 @@ export async function getOnChainBalances< return; } // Update priceRate of wrappedToken - subgraphPools[index].tokens[wrappedIndex].priceRate = formatFixed( + poolsToBeCalled[index].tokens[wrappedIndex].priceRate = formatFixed( onchainData.rate, 18 ); } - if (subgraphPools[index].poolType !== 'FX') - subgraphPools[index].swapFee = formatFixed(swapFee, 18); + if (poolsToBeCalled[index].poolType !== 'FX') + poolsToBeCalled[index].swapFee = formatFixed(swapFee, 18); poolTokens.tokens.forEach((token, i) => { - const tokens = subgraphPools[index].tokens; + const tokens = poolsToBeCalled[index].tokens; const T = tokens.find((t) => isSameAddress(t.address, token)); if (!T) throw `Pool Missing Expected Token: ${poolId} ${token}`; T.balance = formatFixed(poolTokens.balances[i], T.decimals); @@ -349,8 +154,8 @@ export async function getOnChainBalances< // Pools with pre minted BPT if ( - subgraphPools[index].poolType.includes('Linear') || - subgraphPools[index].poolType === 'StablePhantom' + poolsToBeCalled[index].poolType.includes('Linear') || + poolsToBeCalled[index].poolType === 'StablePhantom' ) { if (virtualSupply === undefined) { const logger = Logger.getInstance(); @@ -359,21 +164,21 @@ export async function getOnChainBalances< ); return; } - subgraphPools[index].totalShares = formatFixed(virtualSupply, 18); - } else if (subgraphPools[index].poolType === 'ComposableStable') { + poolsToBeCalled[index].totalShares = formatFixed(virtualSupply, 18); + } else if (poolsToBeCalled[index].poolType === 'ComposableStable') { if (actualSupply === undefined) { const logger = Logger.getInstance(); logger.warn(`ComposableStable missing Actual Supply: ${poolId}`); return; } - subgraphPools[index].totalShares = formatFixed(actualSupply, 18); + poolsToBeCalled[index].totalShares = formatFixed(actualSupply, 18); } else { - subgraphPools[index].totalShares = formatFixed(totalSupply, 18); + poolsToBeCalled[index].totalShares = formatFixed(totalSupply, 18); } if ( - subgraphPools[index].poolType === 'GyroE' && - subgraphPools[index].poolTypeVersion == 2 + poolsToBeCalled[index].poolType === 'GyroE' && + poolsToBeCalled[index].poolTypeVersion == 2 ) { if (!Array.isArray(tokenRates) || tokenRates.length !== 2) { console.error( @@ -381,15 +186,219 @@ export async function getOnChainBalances< ); return; } - subgraphPools[index].tokenRates = tokenRates.map((rate) => + poolsToBeCalled[index].tokenRates = tokenRates.map((rate) => formatFixed(rate, 18) ); } - onChainPools.push(subgraphPools[index]); + onChainPools.push(poolsToBeCalled[index]); } catch (err) { throw new Error(`Issue with pool onchain data: ${err}`); } }); return onChainPools; } + +const generateMultipools = ( + pools: GenericPool[], + vaultAddress: string, + multiAddress: string, + provider: Provider, + chunkSize?: number +): { + multipools: Multicaller[]; + poolsToBeCalled: GenericPool[]; +} => { + const abis: JsonFragment[] = _.uniqBy( + [ + ...(Vault__factory.abi as readonly JsonFragment[]), + ...(StaticATokenRateProvider__factory.abi as readonly JsonFragment[]), + ...(WeightedPool__factory.abi as readonly JsonFragment[]), + ...(StablePool__factory.abi as readonly JsonFragment[]), + ...(ConvergentCurvePool__factory.abi as readonly JsonFragment[]), + ...(LinearPool__factory.abi as readonly JsonFragment[]), + ...(ComposableStablePool__factory.abi as readonly JsonFragment[]), + ...(GyroEV2__factory.abi as readonly JsonFragment[]), + ], + 'name' + ); + const supportedPoolTypes: string[] = Object.values(PoolType); + + if (!chunkSize) { + chunkSize = pools.length; + } + + const chunks: GenericPool[][] = []; + for (let i = 0; i < pools.length / chunkSize; i += 1) { + const chunk = pools.slice(i * chunkSize, (i + 1) * chunkSize); + chunks.push(chunk); + } + const poolsToBeCalled: GenericPool[] = []; + const multicallers: Multicaller[] = chunks.map((poolsChunk) => { + const multicall = Multicall__factory.connect(multiAddress, provider); + const multiPool = new Multicaller(multicall, abis); + poolsChunk.forEach((pool) => { + if ( + !supportedPoolTypes.includes(pool.poolType) || + pool.poolType === 'Managed' + ) { + const logger = Logger.getInstance(); + logger.warn(`Unknown pool type: ${pool.poolType} ${pool.id}`); + return; + } + + poolsToBeCalled.push(pool); + + multiPool.call(`${pool.id}.poolTokens`, vaultAddress, 'getPoolTokens', [ + pool.id, + ]); + multiPool.call(`${pool.id}.totalSupply`, pool.address, 'totalSupply'); + + switch (pool.poolType) { + case 'LiquidityBootstrapping': + case 'Investment': + case 'Weighted': + multiPool.call( + `${pool.id}.swapFee`, + pool.address, + 'getSwapFeePercentage' + ); + multiPool.call( + `${pool.id}.weights`, + pool.address, + 'getNormalizedWeights' + ); + break; + case 'StablePhantom': + multiPool.call( + `${pool.id}.virtualSupply`, + pool.address, + 'getVirtualSupply' + ); + multiPool.call( + `${pool.id}.amp`, + pool.address, + 'getAmplificationParameter' + ); + multiPool.call( + `${pool.id}.swapFee`, + pool.address, + 'getSwapFeePercentage' + ); + break; + // MetaStable is the same as Stable for multicall purposes + case 'MetaStable': + case 'Stable': + multiPool.call( + `${pool.id}.amp`, + pool.address, + 'getAmplificationParameter' + ); + multiPool.call( + `${pool.id}.swapFee`, + pool.address, + 'getSwapFeePercentage' + ); + break; + case 'ComposableStable': + /** + * Returns the effective BPT supply. + * In other pools, this would be the same as `totalSupply`, but there are two key differences here: + * - this pool pre-mints BPT and holds it in the Vault as a token, and as such we need to subtract the Vault's + * balance to get the total "circulating supply". This is called the 'virtualSupply'. + * - the Pool owes debt to the Protocol in the form of unminted BPT, which will be minted immediately before the + * next join or exit. We need to take these into account since, even if they don't yet exist, they will + * effectively be included in any Pool operation that involves BPT. + * In the vast majority of cases, this function should be used instead of `totalSupply()`. + */ + multiPool.call( + `${pool.id}.actualSupply`, + pool.address, + 'getActualSupply' + ); + // MetaStable & StablePhantom is the same as Stable for multicall purposes + multiPool.call( + `${pool.id}.amp`, + pool.address, + 'getAmplificationParameter' + ); + multiPool.call( + `${pool.id}.swapFee`, + pool.address, + 'getSwapFeePercentage' + ); + break; + case 'Element': + multiPool.call(`${pool.id}.swapFee`, pool.address, 'percentFee'); + break; + case 'Gyro2': + case 'Gyro3': + multiPool.call( + `${pool.id}.poolTokens`, + vaultAddress, + 'getPoolTokens', + [pool.id] + ); + multiPool.call(`${pool.id}.totalSupply`, pool.address, 'totalSupply'); + multiPool.call( + `${pool.id}.swapFee`, + pool.address, + 'getSwapFeePercentage' + ); + break; + case 'GyroE': + multiPool.call( + `${pool.id}.swapFee`, + pool.address, + 'getSwapFeePercentage' + ); + if (pool.poolTypeVersion && pool.poolTypeVersion === 2) { + multiPool.call( + `${pool.id}.tokenRates`, + pool.address, + 'getTokenRates' + ); + } + break; + default: + //Handling all Linear pools + if (pool.poolType.toString().includes('Linear')) { + multiPool.call( + `${pool.id}.virtualSupply`, + pool.address, + 'getVirtualSupply' + ); + multiPool.call( + `${pool.id}.swapFee`, + pool.address, + 'getSwapFeePercentage' + ); + multiPool.call(`${pool.id}.targets`, pool.address, 'getTargets'); + multiPool.call( + `${pool.id}.rate`, + pool.address, + 'getWrappedTokenRate' + ); + } + break; + } + }); + return multiPool; + }); + + return { multipools: multicallers, poolsToBeCalled }; +}; + +const executeChunks = async ( + multipools: Multicaller[] +): Promise> => { + const multicallPools = ( + (await Promise.all(multipools.map((m) => m.execute()))) as Record< + string, + MulticallPool + >[] + ).reduce((acc, poolsRecord) => { + return { ...acc, ...poolsRecord }; + }, {}); + return multicallPools; +}; diff --git a/balancer-js/src/modules/sor/pool-data/subgraphPoolDataService.ts b/balancer-js/src/modules/sor/pool-data/subgraphPoolDataService.ts index 4b94e8fac..a2cc54ff5 100644 --- a/balancer-js/src/modules/sor/pool-data/subgraphPoolDataService.ts +++ b/balancer-js/src/modules/sor/pool-data/subgraphPoolDataService.ts @@ -42,6 +42,7 @@ export function mapPools(pools: any[]): SubgraphPoolBase[] { export class SubgraphPoolDataService implements PoolDataService { private readonly defaultArgs: GraphQLArgs; + constructor( private readonly client: SubgraphClient, private readonly provider: Provider, @@ -70,7 +71,10 @@ export class SubgraphPoolDataService implements PoolDataService { * @param queryArgs * @returns SubgraphPoolBase[] */ - async getPools(queryArgs?: GraphQLArgs): Promise { + async getPools( + queryArgs?: GraphQLArgs, + chunkSize?: number + ): Promise { const pools = await this.getSubgraphPools(queryArgs); const filteredPools = pools.filter((p) => { @@ -93,7 +97,8 @@ export class SubgraphPoolDataService implements PoolDataService { mapped, this.network.addresses.contracts.multicall, this.network.addresses.contracts.vault, - this.provider + this.provider, + chunkSize ); console.timeEnd(`fetching on-chain balances for ${mapped.length} pools`); diff --git a/balancer-js/src/modules/sor/pool-data/types.ts b/balancer-js/src/modules/sor/pool-data/types.ts new file mode 100644 index 000000000..0965c94a3 --- /dev/null +++ b/balancer-js/src/modules/sor/pool-data/types.ts @@ -0,0 +1,17 @@ +import { BigNumberish } from '@ethersproject/bignumber'; + +export type MulticallPool = { + amp?: string[]; + swapFee: string; + weights?: string[]; + poolTokens: { + tokens: string[]; + balances: string[]; + }; + totalSupply: string; + virtualSupply?: string; + rate?: string; + actualSupply?: string; + tokenRates?: string[]; + targets: BigNumberish[]; +}; diff --git a/balancer-js/src/modules/swaps/swaps.module.ts b/balancer-js/src/modules/swaps/swaps.module.ts index 82c131816..b63135b45 100644 --- a/balancer-js/src/modules/swaps/swaps.module.ts +++ b/balancer-js/src/modules/swaps/swaps.module.ts @@ -212,7 +212,6 @@ export class Swaps { gasPrice: BigNumber.from(opts.gasPrice), maxPools: opts.maxPools, }); - const tx = this.buildSwap({ userAddress: sender, // sender account recipient, // recipient account @@ -288,8 +287,11 @@ export class Swaps { * @returns Boolean indicating whether pools data was fetched correctly (true) or not (false). */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - async fetchPools(queryArgs?: GraphQLArgs): Promise { - return this.sor.fetchPools(queryArgs); + async fetchPools( + queryArgs?: GraphQLArgs, + chunkSize?: number + ): Promise { + return this.sor.fetchPools(queryArgs, chunkSize); } public getPools(): SubgraphPoolBase[] {