diff --git a/balancer-js/examples/data/onchain-multicall3.ts b/balancer-js/examples/data/onchain-multicall3.ts new file mode 100644 index 000000000..c91d11f2e --- /dev/null +++ b/balancer-js/examples/data/onchain-multicall3.ts @@ -0,0 +1,66 @@ +import { PoolsSubgraphRepository } from '@/modules/data/pool/subgraph' +import { getOnChainBalances as getOnChainBalances3 } from '@/modules/sor/pool-data/onChainData3' +import { getOnChainBalances } from '@/modules/sor/pool-data/onChainData' +import { JsonRpcProvider } from '@ethersproject/providers' +import _ from 'lodash' + +const pools = new PoolsSubgraphRepository({ + url: 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-v2', + chainId: 1, + query: { + args: { + first: 10, + where: { + poolType: { + eq: "MetaStable" + }, + }, + }, + attrs: {}, + }, +}) + +// const provider = new JsonRpcProvider('https://eth-mainnet.alchemyapi.io/v2/7gYoDJEw6-QyVP5hd2UfZyelzDIDemGz') +const provider = new JsonRpcProvider('http://127.0.0.1:8545') + +function findNestedValueDifferences(object1: any, object2: any, path = ''): any { + const allKeys = _.union(Object.keys(object1), Object.keys(object2)) + + const differences = [] + + for (const key of allKeys) { + const newPath = path ? `${path}.${key}` : key + + if (_.isObject(object1[key]) && _.isObject(object2[key])) { + differences.push(...findNestedValueDifferences(object1[key], object2[key], newPath)) + } else if (!_.isEqual(object1[key], object2[key])) { + differences.push({ + path: newPath, + value1: object1[key], + value2: object2[key] + }) + } + } + + return differences +} + +async function main() { + const subgraph = await pools.fetch(); + const onchain3 = await getOnChainBalances3(subgraph, '', '', provider); + const onchain = await getOnChainBalances(subgraph, '0xeefba1e63905ef1d7acba5a8513c70307c1ce441', '0xBA12222222228d8Ba445958a75a0704d566BF2C8', provider); + for(const i in subgraph) { + const one = onchain3.find((x) => x.id === subgraph[i].id) + const two = onchain.find((x) => x.id === subgraph[i].id) + console.log('Pool', subgraph[i].id) + if (!two) { + console.log('two missing') + continue + } + console.log(JSON.stringify(findNestedValueDifferences(one, two), null, 2)); + } +} + +main() + +// yarn example ./examples/data/onchain-multicall3.ts diff --git a/balancer-js/src/modules/sor/pool-data/onChainData3.ts b/balancer-js/src/modules/sor/pool-data/onChainData3.ts new file mode 100644 index 000000000..53585a08e --- /dev/null +++ b/balancer-js/src/modules/sor/pool-data/onChainData3.ts @@ -0,0 +1,219 @@ +import { Multicaller3 } from '@/lib/utils/multiCaller3'; +import { Provider } from '@ethersproject/providers'; +import { SubgraphPoolBase } from '@/index'; +import { formatFixed } from '@ethersproject/bignumber'; + +const abi = [ + 'function getSwapFeePercentage() view returns (uint256)', + 'function percentFee() view returns (uint256)', + 'function protocolPercentFee() view returns (uint256)', + 'function getNormalizedWeights() view returns (uint256[])', + 'function totalSupply() view returns (uint256)', + 'function getVirtualSupply() view returns (uint256)', + 'function getActualSupply() view returns (uint256)', + 'function getTargets() view returns (uint256 lowerTarget, uint256 upperTarget)', + 'function getTokenRates() view returns (uint256, uint256)', + 'function getWrappedTokenRate() view returns (uint256)', + 'function getAmplificationParameter() view returns (uint256 value, bool isUpdating, uint256 precision)', + 'function getPausedState() view returns (bool)', + 'function inRecoveryMode() view returns (bool)', + 'function getRate() view returns (uint256)', + 'function getScalingFactors() view returns (uint256[] memory)', // do we need this here? + 'function getPoolTokens(bytes32) view returns (address[], uint256[])', +]; + +const getTotalSupplyFn = (poolType: string) => { + if (poolType.includes('Linear') || ['StablePhantom'].includes(poolType)) { + return 'getVirtualSupply'; + } else if (poolType === 'ComposableStable') { + return 'getActualSupply'; + } else { + return 'totalSupply'; + } +}; + +const getSwapFeeFn = (poolType: string) => { + if (poolType === 'Element') { + return 'percentFee'; + } else if (poolType === 'FX') { + return 'protocolPercentFee'; + } else { + return 'getSwapFeePercentage'; + } +}; + +const vaultAddress = '0xBA12222222228d8Ba445958a75a0704d566BF2C8'; + +interface OnchainData { + poolTokens: [string[], string[]]; + totalShares: string; + swapFee: string; + isPaused?: boolean; + inRecoveryMode?: boolean; + rate?: string; + scalingFactors?: string[]; + weights?: string[]; + targets?: [string, string]; + wrappedTokenRate?: string; + amp?: [string, boolean, string]; + tokenRates?: [string, string]; +} + +const defaultCalls = ( + id: string, + address: string, + poolType: string, + multicaller: Multicaller3 +) => { + multicaller.call(`${id}.poolTokens`, vaultAddress, 'getPoolTokens', [id]); + multicaller.call(`${id}.totalShares`, address, getTotalSupplyFn(poolType)); + multicaller.call(`${id}.swapFee`, address, getSwapFeeFn(poolType)); + // multicaller.call(`${id}.isPaused`, address, 'getPausedState'); + // multicaller.call(`${id}.inRecoveryMode`, address, 'inRecoveryMode'); + // multicaller.call(`${id}.rate`, address, 'getRate'); + // multicaller.call(`${id}.scalingFactors`, address, 'getScalingFactors'); +}; + +const weightedCalls = ( + id: string, + address: string, + multicaller: Multicaller3 +) => { + multicaller.call(`${id}.weights`, address, 'getNormalizedWeights'); +}; + +const linearCalls = ( + id: string, + address: string, + multicaller: Multicaller3 +) => { + multicaller.call(`${id}.targets`, address, 'getTargets'); + multicaller.call(`${id}.wrappedTokenRate`, address, 'getWrappedTokenRate'); +}; + +const stableCalls = ( + id: string, + address: string, + multicaller: Multicaller3 +) => { + multicaller.call(`${id}.amp`, address, 'getAmplificationParameter'); +}; + +const gyroECalls = (id: string, address: string, multicaller: Multicaller3) => { + multicaller.call(`${id}.tokenRates`, address, 'getTokenRates'); +}; + +const poolTypeCalls = (poolType: string) => { + switch (poolType) { + case 'Weighted': + case 'LiquidityBootstrapping': + case 'Investment': + return weightedCalls; + case 'Stable': + case 'StablePhantom': + case 'MetaStable': + case 'ComposableStable': + return stableCalls; + case 'GyroE': + return gyroECalls; + default: + if (poolType.includes('Linear')) { + return linearCalls; + } else { + return () => ({}); // do nothing + } + } +}; + +const merge = (pool: SubgraphPoolBase, result: OnchainData) => ({ + ...pool, + tokens: pool.tokens.map((token) => { + const idx = result.poolTokens[0] + .map((t) => t.toLowerCase()) + .indexOf(token.address); + const wrappedToken = + pool.wrappedIndex && pool.tokensList[pool.wrappedIndex]; + return { + ...token, + balance: formatFixed(result.poolTokens[1][idx], token.decimals || 18), + weight: + (result.weights && formatFixed(result.weights[idx], 18)) || + token.weight, + priceRate: + (result.wrappedTokenRate && + wrappedToken && + wrappedToken.toLowerCase() === token.address.toLowerCase() && + formatFixed(result.wrappedTokenRate, 18)) || + token.priceRate || + '0', + }; + }), + totalShares: formatFixed(result.totalShares, 18), + swapFee: formatFixed(result.swapFee, 18), + amp: + (result.amp && + result.amp[0] && + formatFixed(result.amp[0], String(result.amp[2]).length - 1)) || + undefined, + lowerTarget: + (result.targets && formatFixed(result.targets[0], 18)) || undefined, + upperTarget: + (result.targets && formatFixed(result.targets[1], 18)) || undefined, + tokenRates: + (result.tokenRates && + result.tokenRates.map((rate) => formatFixed(rate, 18))) || + undefined, + // rate: result.rate, + // isPaused: result.isPaused, + // inRecoveryMode: result.inRecoveryMode, + // scalingFactors: result.scalingFactors, +}); + +export const fetchOnChainPoolData = async ( + pools: { + id: string; + address: string; + poolType: string; + }[], + provider: Provider +): Promise<{ [id: string]: OnchainData }> => { + if (pools.length === 0) { + return {}; + } + + const multicaller = new Multicaller3(abi, provider); + + pools.forEach(({ id, address, poolType }) => { + defaultCalls(id, address, poolType, multicaller); + poolTypeCalls(poolType)(id, address, multicaller); + }); + + const results = (await multicaller.execute({}, 100)) as { + [id: string]: OnchainData; + }; + + return results; +}; + +export async function getOnChainBalances( + subgraphPoolsOriginal: SubgraphPoolBase[], + _multiAddress: string, + _vaultAddress: string, + provider: Provider +): Promise { + if (subgraphPoolsOriginal.length === 0) return subgraphPoolsOriginal; + + const poolsWithOnchainData: SubgraphPoolBase[] = []; + + const onchainData = (await fetchOnChainPoolData( + subgraphPoolsOriginal, + provider + )) as { [id: string]: OnchainData }; + + subgraphPoolsOriginal.forEach((pool) => { + const data = onchainData[pool.id]; + poolsWithOnchainData.push(merge(pool, data)); + }); + + return poolsWithOnchainData; +} diff --git a/balancer-js/src/modules/sor/pool-data/subgraphPoolDataService.ts b/balancer-js/src/modules/sor/pool-data/subgraphPoolDataService.ts index 86dbcef33..94c0b1af7 100644 --- a/balancer-js/src/modules/sor/pool-data/subgraphPoolDataService.ts +++ b/balancer-js/src/modules/sor/pool-data/subgraphPoolDataService.ts @@ -6,7 +6,7 @@ import { SubgraphClient, } from '@/modules/subgraph/subgraph'; import { parseInt } from 'lodash'; -import { getOnChainBalances } from './onChainData'; +import { getOnChainBalances } from './onChainData3'; import { Provider } from '@ethersproject/providers'; import { BalancerNetworkConfig,