diff --git a/modules/network/gnosis.ts b/modules/network/gnosis.ts index 1f16d6628..8df1f6612 100644 --- a/modules/network/gnosis.ts +++ b/modules/network/gnosis.ts @@ -53,7 +53,9 @@ const gnosisNetworkData: NetworkData = { excludedTokenAddresses: [], }, rpcUrl: - (env.DEPLOYMENT_ENV as DeploymentEnv) === 'main' ? `https://rpc.gnosis.gateway.fm` : 'https://gnosis.drpc.org', + (env.DEPLOYMENT_ENV as DeploymentEnv) === 'main' + ? `https://rpc.eu-central-2.gateway.fm/v4/gnosis/non-archival/mainnet` + : 'https://gnosis.drpc.org', rpcMaxBlockRange: 2000, protocolToken: 'bal', bal: { diff --git a/modules/network/mainnet.ts b/modules/network/mainnet.ts index 321163fba..28189685d 100644 --- a/modules/network/mainnet.ts +++ b/modules/network/mainnet.ts @@ -349,6 +349,17 @@ export const data: NetworkData = { path: 'value', isIbYield: true, }, + sDOLA: { + tokenAddress: '0xb45ad160634c528cc3d2926d9807104fa3157305', + sourceUrl: 'https://www.inverse.finance/api/dola-staking', + path: 'apr', + isIbYield: true, + }, + rswETH: { + tokenAddress: '0xfae103dc9cf190ed75350761e95403b7b8afa6c0', + sourceUrl: 'https://v3-lrt.svc.swellnetwork.io/api/tokens/rsweth/apr', + isIbYield: true, + }, }, }, beefy: { diff --git a/modules/pool/abi/BalancerQueries.json b/modules/pool/abi/BalancerQueries.json new file mode 100644 index 000000000..4f23e1f2b --- /dev/null +++ b/modules/pool/abi/BalancerQueries.json @@ -0,0 +1,309 @@ +[ + { + "inputs": [ + { + "internalType": "contract IVault", + "name": "_vault", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "enum IVault.SwapKind", + "name": "kind", + "type": "uint8" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "assetInIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "assetOutIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IVault.BatchSwapStep[]", + "name": "swaps", + "type": "tuple[]" + }, + { + "internalType": "contract IAsset[]", + "name": "assets", + "type": "address[]" + }, + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bool", + "name": "fromInternalBalance", + "type": "bool" + }, + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + }, + { + "internalType": "bool", + "name": "toInternalBalance", + "type": "bool" + } + ], + "internalType": "struct IVault.FundManagement", + "name": "funds", + "type": "tuple" + } + ], + "name": "queryBatchSwap", + "outputs": [ + { + "internalType": "int256[]", + "name": "assetDeltas", + "type": "int256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "components": [ + { + "internalType": "contract IAsset[]", + "name": "assets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "minAmountsOut", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + }, + { + "internalType": "bool", + "name": "toInternalBalance", + "type": "bool" + } + ], + "internalType": "struct IVault.ExitPoolRequest", + "name": "request", + "type": "tuple" + } + ], + "name": "queryExit", + "outputs": [ + { + "internalType": "uint256", + "name": "bptIn", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "amountsOut", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "components": [ + { + "internalType": "contract IAsset[]", + "name": "assets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "maxAmountsIn", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + }, + { + "internalType": "bool", + "name": "fromInternalBalance", + "type": "bool" + } + ], + "internalType": "struct IVault.JoinPoolRequest", + "name": "request", + "type": "tuple" + } + ], + "name": "queryJoin", + "outputs": [ + { + "internalType": "uint256", + "name": "bptOut", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "amountsIn", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "enum IVault.SwapKind", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "contract IAsset", + "name": "assetIn", + "type": "address" + }, + { + "internalType": "contract IAsset", + "name": "assetOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IVault.SingleSwap", + "name": "singleSwap", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bool", + "name": "fromInternalBalance", + "type": "bool" + }, + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + }, + { + "internalType": "bool", + "name": "toInternalBalance", + "type": "bool" + } + ], + "internalType": "struct IVault.FundManagement", + "name": "funds", + "type": "tuple" + } + ], + "name": "querySwap", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "vault", + "outputs": [ + { + "internalType": "contract IVault", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/modules/pool/lib/pool-gql-loader.service.ts b/modules/pool/lib/pool-gql-loader.service.ts index 5d6305758..8be100aba 100644 --- a/modules/pool/lib/pool-gql-loader.service.ts +++ b/modules/pool/lib/pool-gql-loader.service.ts @@ -44,10 +44,9 @@ import { networkContext } from '../../network/network-context.service'; import { fixedNumber } from '../../view-helpers/fixed-number'; import { parseUnits } from 'ethers/lib/utils'; import { formatFixed } from '@ethersproject/bignumber'; -import { BalancerChainIds, BeethovenChainIds, chainIdToChain, chainToIdMap } from '../../network/network-config'; +import { BeethovenChainIds, chainToIdMap } from '../../network/network-config'; import { GithubContentService } from '../../content/github-content.service'; import { SanityContentService } from '../../content/sanity-content.service'; -import { FeaturedPool } from '../../content/content-types'; import { ElementData, FxData, GyroData, LinearData, StableData } from '../subgraph-mapper'; export class PoolGqlLoaderService { diff --git a/modules/pool/lib/pool-on-chain-data.service.ts b/modules/pool/lib/pool-on-chain-data.service.ts index 1c23bb203..326fed052 100644 --- a/modules/pool/lib/pool-on-chain-data.service.ts +++ b/modules/pool/lib/pool-on-chain-data.service.ts @@ -1,5 +1,5 @@ import { formatFixed } from '@ethersproject/bignumber'; -import { PrismaPoolType } from '@prisma/client'; +import { Prisma, PrismaPoolType } from '@prisma/client'; import { isSameAddress } from '@balancer-labs/sdk'; import { prisma } from '../../../prisma/prisma-client'; import { isStablePool } from './pool-utils'; @@ -10,6 +10,7 @@ import { fetchOnChainPoolData } from './pool-onchain-data'; import { fetchOnChainGyroFees } from './pool-onchain-gyro-fee'; import { networkContext } from '../../network/network-context.service'; import { LinearData, StableData } from '../subgraph-mapper'; +import { fetchTokenPairData } from './pool-on-chain-tokenpair-data'; const SUPPORTED_POOL_TYPES: PrismaPoolType[] = [ 'WEIGHTED', @@ -33,6 +34,7 @@ export class PoolOnChainDataService { return { chain: networkContext.chain, vaultAddress: networkContext.data.balancer.v2.vaultAddress, + balancerQueriesAddress: networkContext.data.balancer.v2.balancerQueriesAddress, yieldProtocolFeePercentage: networkContext.data.balancer.v2.defaultSwapFeePercentage, swapProtocolFeePercentage: networkContext.data.balancer.v2.defaultSwapFeePercentage, gyroConfig: networkContext.data.gyro?.config, @@ -99,7 +101,12 @@ export class PoolOnChainDataService { const onchainResults = await fetchOnChainPoolData( filteredPools, this.options.vaultAddress, - networkContext.chain === 'ZKEVM' ? 190 : 1024, + this.options.chain === 'ZKEVM' ? 190 : 1024, + ); + const tokenPairData = await fetchTokenPairData( + filteredPools, + this.options.balancerQueriesAddress, + this.options.chain === 'ZKEVM' ? 190 : 1024, ); const gyroFees = await (this.options.gyroConfig ? fetchOnChainGyroFees(gyroPools, this.options.gyroConfig, networkContext.chain === 'ZKEVM' ? 190 : 1024) @@ -108,6 +115,7 @@ export class PoolOnChainDataService { const operations = []; for (const pool of filteredPools) { const onchainData = onchainResults[pool.id]; + const { tokenPairs } = tokenPairData[pool.id]; const { amp, poolTokens } = onchainData; try { @@ -201,6 +209,18 @@ export class PoolOnChainDataService { ); } + // always update tokenPair data + if (pool.dynamicData) { + operations.push( + prisma.prismaPoolDynamicData.update({ + where: { id_chain: { id: pool.id, chain: this.options.chain } }, + data: { + tokenPairsData: tokenPairs, + }, + }), + ); + } + for (let i = 0; i < poolTokens.tokens.length; i++) { const tokenAddress = poolTokens.tokens[i]; const poolToken = pool.tokens.find((token) => isSameAddress(token.address, tokenAddress)); diff --git a/modules/pool/lib/pool-on-chain-tokenpair-data.ts b/modules/pool/lib/pool-on-chain-tokenpair-data.ts new file mode 100644 index 000000000..81c90e769 --- /dev/null +++ b/modules/pool/lib/pool-on-chain-tokenpair-data.ts @@ -0,0 +1,305 @@ +import { Multicaller3 } from '../../web3/multicaller3'; +import { BigNumber } from '@ethersproject/bignumber'; +import BalancerQueries from '../abi/BalancerQueries.json'; +import { MathSol, WAD, ZERO_ADDRESS } from '@balancer/sdk'; +import { parseEther, parseUnits } from 'viem'; +import * as Sentry from '@sentry/node'; + +interface PoolInput { + id: string; + address: string; + tokens: { + address: string; + token: { + decimals: number; + }; + dynamicData: { + balance: string; + balanceUSD: number; + } | null; + }[]; + dynamicData: { + totalLiquidity: number; + } | null; +} + +interface PoolTokenPairsOutput { + [poolId: string]: { + tokenPairs: TokenPairData[]; + }; +} + +export type TokenPairData = { + tokenA: string; + tokenB: string; + normalizedLiquidity: string; + spotPrice: string; +}; + +interface TokenPair { + poolId: string; + poolTvl: number; + valid: boolean; + tokenA: Token; + tokenB: Token; + normalizedLiqudity: bigint; + spotPrice: bigint; + aToBAmountIn: bigint; + aToBAmountOut: bigint; + bToAAmountOut: bigint; + effectivePrice: bigint; + effectivePriceAmountIn: bigint; +} + +interface Token { + address: string; + decimals: number; + balance: string; + balanceUsd: number; +} + +interface OnchainData { + effectivePriceAmountOut: BigNumber; + aToBAmountOut: BigNumber; + bToAAmountOut: BigNumber; +} + +export async function fetchTokenPairData(pools: PoolInput[], balancerQueriesAddress: string, batchSize = 1024) { + if (pools.length === 0) { + return {}; + } + + const tokenPairOutput: PoolTokenPairsOutput = {}; + + const multicaller = new Multicaller3(BalancerQueries, batchSize); + + // only inlcude pools with TVL >=$1000 + // for each pool, get pairs + // for each pair per pool, create multicall to do a swap with $200 (min liq is $1k, so there should be at least $200 for each token) for effectivePrice calc and a swap with 1% TVL + // then create multicall to do the second swap for each pair using the result of the first 1% swap as input, to calculate the spot price + // https://github.com/balancer/b-sdk/pull/204/files#diff-52e6d86a27aec03f59dd3daee140b625fd99bd9199936bbccc50ee550d0b0806 + + const tokenPairs = generateTokenPairs(pools); + + tokenPairs.forEach((tokenPair) => { + if (tokenPair.valid) { + // prepare swap amounts in + // tokenA->tokenB with 1% of tokenA balance + tokenPair.aToBAmountIn = parseUnits(tokenPair.tokenA.balance, tokenPair.tokenA.decimals) / 100n; + // tokenA->tokenB with 100USD worth of tokenA + const oneHundredUsdOfTokenA = (parseFloat(tokenPair.tokenA.balance) / tokenPair.tokenA.balanceUsd) * 100; + tokenPair.effectivePriceAmountIn = parseUnits(`${oneHundredUsdOfTokenA}`, tokenPair.tokenA.decimals); + + addEffectivePriceCallsToMulticaller(tokenPair, balancerQueriesAddress, multicaller); + addAToBPriceCallsToMulticaller(tokenPair, balancerQueriesAddress, multicaller); + } + }); + + const resultOne = (await multicaller.execute()) as { + [id: string]: OnchainData; + }; + + tokenPairs.forEach((tokenPair) => { + if (tokenPair.valid) { + getAmountOutAndEffectivePriceFromResult(tokenPair, resultOne); + } + }); + + tokenPairs.forEach((tokenPair) => { + if (tokenPair.valid) { + addBToAPriceCallsToMulticaller(tokenPair, balancerQueriesAddress, multicaller); + } + }); + + const resultTwo = (await multicaller.execute()) as { + [id: string]: OnchainData; + }; + + tokenPairs.forEach((tokenPair) => { + if (tokenPair.valid) { + getBToAAmountFromResult(tokenPair, resultTwo); + calculateSpotPrice(tokenPair); + calculateNormalizedLiquidity(tokenPair); + } + + // prepare output + pools.forEach((pool) => { + if (pool.id === tokenPair.poolId) { + if (!tokenPairOutput[pool.id]) { + tokenPairOutput[pool.id] = { + tokenPairs: [], + }; + } + tokenPairOutput[pool.id].tokenPairs.push({ + tokenA: tokenPair.tokenA.address, + tokenB: tokenPair.tokenB.address, + normalizedLiquidity: tokenPair.normalizedLiqudity.toString(), + spotPrice: tokenPair.spotPrice.toString(), + }); + } + }); + }); + + return tokenPairOutput; +} + +function generateTokenPairs(filteredPools: PoolInput[]): TokenPair[] { + const tokenPairs: TokenPair[] = []; + + for (const pool of filteredPools) { + // create all pairs for pool + for (let i = 0; i < pool.tokens.length - 1; i++) { + for (let j = i + 1; j < pool.tokens.length; j++) { + //skip pairs with phantom BPT + if (pool.tokens[i].address === pool.address || pool.tokens[j].address === pool.address) continue; + tokenPairs.push({ + poolId: pool.id, + poolTvl: pool.dynamicData?.totalLiquidity || 0, + // remove pools that have <$1000 TVL or a token without a balance or USD balance + valid: + (pool.dynamicData?.totalLiquidity || 0) >= 1000 && + !pool.tokens.some((token) => token.dynamicData?.balance || '0' === '0') && + !pool.tokens.some((token) => token.dynamicData?.balanceUSD || 0 === 0), + + tokenA: { + address: pool.tokens[i].address, + decimals: pool.tokens[i].token.decimals, + balance: pool.tokens[i].dynamicData?.balance || '0', + balanceUsd: pool.tokens[i].dynamicData?.balanceUSD || 0, + }, + tokenB: { + address: pool.tokens[j].address, + decimals: pool.tokens[j].token.decimals, + balance: pool.tokens[j].dynamicData?.balance || '0', + balanceUsd: pool.tokens[j].dynamicData?.balanceUSD || 0, + }, + normalizedLiqudity: 0n, + spotPrice: 0n, + aToBAmountIn: 0n, + aToBAmountOut: 0n, + bToAAmountOut: 0n, + effectivePrice: 0n, + effectivePriceAmountIn: 0n, + }); + } + } + } + return tokenPairs; +} + +// call querySwap from tokenA->tokenB with 100USD worth of tokenA +function addEffectivePriceCallsToMulticaller( + tokenPair: TokenPair, + balancerQueriesAddress: string, + multicaller: Multicaller3, +) { + multicaller.call( + `${tokenPair.poolId}-${tokenPair.tokenA.address}-${tokenPair.tokenB.address}.effectivePriceAmountOut`, + balancerQueriesAddress, + 'querySwap', + [ + [ + tokenPair.poolId, + 0, + tokenPair.tokenA.address, + tokenPair.tokenB.address, + `${tokenPair.effectivePriceAmountIn}`, + ZERO_ADDRESS, + ], + [ZERO_ADDRESS, false, ZERO_ADDRESS, false], + ], + ); +} + +// call querySwap from tokenA->tokenB with 1% of tokenA balance +function addAToBPriceCallsToMulticaller( + tokenPair: TokenPair, + balancerQueriesAddress: string, + multicaller: Multicaller3, +) { + multicaller.call( + `${tokenPair.poolId}-${tokenPair.tokenA.address}-${tokenPair.tokenB.address}.aToBAmountOut`, + balancerQueriesAddress, + 'querySwap', + [ + [ + tokenPair.poolId, + 0, + tokenPair.tokenA.address, + tokenPair.tokenB.address, + `${tokenPair.aToBAmountIn}`, + ZERO_ADDRESS, + ], + [ZERO_ADDRESS, false, ZERO_ADDRESS, false], + ], + ); +} + +// call querySwap from tokenA->tokenB with AtoB amount out +function addBToAPriceCallsToMulticaller( + tokenPair: TokenPair, + balancerQueriesAddress: string, + multicaller: Multicaller3, +) { + multicaller.call( + `${tokenPair.poolId}-${tokenPair.tokenA.address}-${tokenPair.tokenB.address}.bToAAmountOut`, + balancerQueriesAddress, + 'querySwap', + [ + [ + tokenPair.poolId, + 0, + tokenPair.tokenB.address, + tokenPair.tokenA.address, + `${tokenPair.aToBAmountOut}`, + ZERO_ADDRESS, + ], + [ZERO_ADDRESS, false, ZERO_ADDRESS, false], + ], + ); +} + +function getAmountOutAndEffectivePriceFromResult(tokenPair: TokenPair, onchainResults: { [id: string]: OnchainData }) { + const result = onchainResults[`${tokenPair.poolId}-${tokenPair.tokenA.address}-${tokenPair.tokenB.address}`]; + + if (result) { + tokenPair.aToBAmountOut = BigInt(result.aToBAmountOut.toString()); + // MathSol expects all values with 18 decimals, need to scale them + tokenPair.effectivePrice = MathSol.divDownFixed( + parseUnits(tokenPair.effectivePriceAmountIn.toString(), 18 - tokenPair.tokenA.decimals), + parseUnits(result.effectivePriceAmountOut.toString(), 18 - tokenPair.tokenB.decimals), + ); + } +} + +function getBToAAmountFromResult(tokenPair: TokenPair, onchainResults: { [id: string]: OnchainData }) { + const result = onchainResults[`${tokenPair.poolId}-${tokenPair.tokenA.address}-${tokenPair.tokenB.address}`]; + + if (result) { + tokenPair.bToAAmountOut = BigInt(result.bToAAmountOut.toString()); + } +} +function calculateSpotPrice(tokenPair: TokenPair) { + // MathSol expects all values with 18 decimals, need to scale them + const aToBAmountInScaled = parseUnits(tokenPair.aToBAmountIn.toString(), 18 - tokenPair.tokenA.decimals); + const aToBAmountOutScaled = parseUnits(tokenPair.aToBAmountOut.toString(), 18 - tokenPair.tokenB.decimals); + const bToAAmountOutScaled = parseUnits(tokenPair.bToAAmountOut.toString(), 18 - tokenPair.tokenA.decimals); + const priceAtoB = MathSol.divDownFixed(aToBAmountInScaled, aToBAmountOutScaled); + const priceBtoA = MathSol.divDownFixed(aToBAmountOutScaled, bToAAmountOutScaled); + tokenPair.spotPrice = MathSol.powDownFixed(MathSol.divDownFixed(priceAtoB, priceBtoA), WAD / 2n); +} + +function calculateNormalizedLiquidity(tokenPair: TokenPair) { + // spotPrice and effective price are already scaled to 18 decimals by the MathSol output + let priceRatio = MathSol.divDownFixed(tokenPair.spotPrice, tokenPair.effectivePrice); + // if priceRatio is = 1, normalizedLiquidity becomes infinity, if it is >1, normalized liqudity becomes negative. Need to cap it. + // this happens if you get a "bonus" ie positive price impact. + if (priceRatio > parseEther('0.999999')) { + Sentry.captureException( + `Price ratio was > 0.999999 for token pair ${tokenPair.tokenA.address}/${tokenPair.tokenB.address} in pool ${tokenPair.poolId}.`, + ); + priceRatio = parseEther('0.999999'); + } + const priceImpact = WAD - priceRatio; + tokenPair.normalizedLiqudity = MathSol.divDownFixed(WAD, priceImpact); +} diff --git a/modules/pool/pool.prisma b/modules/pool/pool.prisma index b382b3bf8..1d3227782 100644 --- a/modules/pool/pool.prisma +++ b/modules/pool/pool.prisma @@ -115,6 +115,8 @@ model PrismaPoolDynamicData { fees24hAthTimestamp Int @default(0) fees24hAtl Float @default(0) fees24hAtlTimestamp Int @default(0) + + tokenPairsData Json @default("[]") } model PrismaPoolToken { diff --git a/modules/protocol/protocol.service.ts b/modules/protocol/protocol.service.ts index 0dc5cf39a..106d58969 100644 --- a/modules/protocol/protocol.service.ts +++ b/modules/protocol/protocol.service.ts @@ -4,11 +4,12 @@ import { Cache } from 'memory-cache'; import { Chain, PrismaLastBlockSyncedCategory, PrismaUserBalanceType } from '@prisma/client'; import _ from 'lodash'; import { networkContext } from '../network/network-context.service'; -import { AllNetworkConfigsKeyedOnChain } from '../network/network-config'; +import { AllNetworkConfigs, AllNetworkConfigsKeyedOnChain } from '../network/network-config'; import { GqlProtocolMetricsAggregated, GqlProtocolMetricsChain } from '../../schema'; import { GraphQLClient } from 'graphql-request'; import { getSdk } from '../subgraphs/balancer-subgraph/generated/balancer-subgraph-types'; import axios from 'axios'; +import { tokenService } from '../token/token.service'; interface LatestSyncedBlocks { userWalletSyncBlock: string; @@ -110,10 +111,11 @@ export class ProtocolService { const yieldCapture24h = _.sumBy(pools, (pool) => (!pool.dynamicData ? 0 : pool.dynamicData.yieldCapture24h)); const balancerV1Tvl = await this.getBalancerV1Tvl(`${AllNetworkConfigsKeyedOnChain[chain].data.chain.id}`); + const sftmxTvl = await this.getSftmXTVL(`${AllNetworkConfigsKeyedOnChain[chain].data.chain.id}`); const protocolData = { chainId: `${AllNetworkConfigsKeyedOnChain[chain].data.chain.id}`, - totalLiquidity: `${totalLiquidity + balancerV1Tvl}`, + totalLiquidity: `${totalLiquidity + balancerV1Tvl + sftmxTvl}`, totalSwapFee, totalSwapVolume, poolCount: `${poolCount}`, @@ -148,6 +150,23 @@ export class ProtocolService { }; } + private async getSftmXTVL(chainId: string): Promise { + if (chainId !== '250') { + return 0; + } + + const tokenprices = await tokenService.getTokenPrices(AllNetworkConfigs[chainId].data.chain.prismaId); + const ftmPrice = tokenService.getPriceForToken(tokenprices, AllNetworkConfigs[chainId].data.weth.address); + + if (AllNetworkConfigs[chainId].data.sftmx) { + const stakingData = await prisma.prismaSftmxStakingData.findUniqueOrThrow({ + where: { id: AllNetworkConfigs[chainId].data.sftmx!.stakingContractAddress }, + }); + return parseFloat(stakingData.totalFtm) * ftmPrice; + } + return 0; + } + private async getBalancerV1Tvl(chainId: string): Promise { if (chainId !== '1') { return 0; diff --git a/modules/sor/sor.gql b/modules/sor/sor.gql index b9f8a1acb..658b15aaf 100644 --- a/modules/sor/sor.gql +++ b/modules/sor/sor.gql @@ -1,4 +1,7 @@ extend type Query { + """ + Get swap quote from the SOR, queries both the old and new SOR + """ sorGetSwaps( chain: GqlChain tokenIn: String! @@ -7,22 +10,38 @@ extend type Query { swapAmount: BigDecimal! #expected in human readable form swapOptions: GqlSorSwapOptionsInput! ): GqlSorGetSwapsResponse! - sorV2GetSwaps( + """ + Get swap quote from the SOR v2 for the V2 vault + """ + sorGetSwapPaths( + """ + The Chain to query + """ chain: GqlChain! + """ + Token address of the tokenIn + """ tokenIn: String! tokenOut: String! swapType: GqlSorSwapType! swapAmount: BigDecimal! #expected in human readable form - swapOptions: GqlSorSwapOptionsInput! - ): GqlSorGetSwaps! + 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 + ): GqlSorGetSwapPaths! } -type GqlSorGetSwaps { +type GqlSorGetSwapPaths { + vaultVersion: Int! + """ + The token address of the tokenIn provided + """ tokenIn: String! + """ + The token address of the tokenOut provided + """ tokenOut: String! - tokenAddresses: [String!]! - swapType: GqlSorSwapType! - swaps: [GqlSorSwap!]! + swaps: [GqlSorSwap!]! #used by cowswap + paths: [GqlSorPath!]! #used by b-sdk tokenInAmount: AmountHumanReadable! tokenOutAmount: AmountHumanReadable! swapAmount: AmountHumanReadable! @@ -35,6 +54,19 @@ type GqlSorGetSwaps { priceImpact: AmountHumanReadable! } +type GqlSorPath { + vaultVersion: Int! + pools: [String]! #list of pool Ids + tokens: [Token]! + outputAmountRaw: String! + inputAmountRaw: String! +} + +type Token { + address: String! + decimals: Int! +} + enum GqlSorSwapType { EXACT_IN EXACT_OUT @@ -100,6 +132,7 @@ type GqlSorGetSwapsResponse { priceImpact: AmountHumanReadable! } +#used by cowswap type GqlSorSwap { poolId: String! assetInIndex: Int! diff --git a/modules/sor/sor.resolvers.ts b/modules/sor/sor.resolvers.ts index 5d78b973c..b3e18bbe2 100644 --- a/modules/sor/sor.resolvers.ts +++ b/modules/sor/sor.resolvers.ts @@ -16,8 +16,8 @@ const balancerSdkResolvers: Resolvers = { return sorService.getSorSwaps(args); }, - sorV2GetSwaps: async (parent, args, context) => { - return sorService.getSorV2Swaps(args); + sorGetSwapPaths: async (parent, args, context) => { + return sorService.getSorSwapPaths(args); }, }, }; diff --git a/modules/sor/sor.service.ts b/modules/sor/sor.service.ts index 7b87d90df..46adb6397 100644 --- a/modules/sor/sor.service.ts +++ b/modules/sor/sor.service.ts @@ -2,8 +2,8 @@ import { GqlSorSwapType, GqlSorGetSwapsResponse, QuerySorGetSwapsArgs, - GqlSorGetSwaps, - QuerySorV2GetSwapsArgs, + QuerySorGetSwapPathsArgs, + GqlSorGetSwapPaths, } from '../../schema'; import { sorV1BeetsService } from './sorV1Beets/sorV1Beets.service'; import { sorV2Service } from './sorV2/sorV2.service'; @@ -12,15 +12,15 @@ import * as Sentry from '@sentry/node'; import { Chain } from '@prisma/client'; import { parseUnits, formatUnits } from '@ethersproject/units'; import { tokenService } from '../token/token.service'; -import { getToken, getTokenAmountHuman, zeroResponse, zeroResponseV2 } from './utils'; +import { getToken, getTokenAmountHuman, zeroResponse, swapPathsZeroResponse } from './utils'; export class SorService { - async getSorV2Swaps(args: QuerySorV2GetSwapsArgs): Promise { + async getSorSwapPaths(args: QuerySorGetSwapPathsArgs): Promise { console.log('getSorSwaps args', JSON.stringify(args)); const tokenIn = args.tokenIn.toLowerCase(); const tokenOut = args.tokenOut.toLowerCase(); const amountToken = args.swapType === 'EXACT_IN' ? tokenIn : tokenOut; - const emptyResponse = zeroResponseV2(args.swapType, args.tokenIn, args.tokenOut); + const emptyResponse = swapPathsZeroResponse(args.tokenIn, args.tokenOut); // check if tokens addresses exist try { @@ -44,13 +44,13 @@ export class SorService { // args.swapAmount is HumanScale const amount = await getTokenAmountHuman(amountToken, args.swapAmount, args.chain!); - return sorV2Service.getSorSwaps({ + return sorV2Service.getSorSwapPaths({ chain: args.chain!, swapAmount: amount, - swapOptions: args.swapOptions, swapType: args.swapType, tokenIn: tokenIn, tokenOut: tokenOut, + queryBatchSwap: args.queryBatchSwap ? args.queryBatchSwap : true, }); } diff --git a/modules/sor/sorV2/beetsHelpers.ts b/modules/sor/sorV2/beetsHelpers.ts index 6a7226742..e0e9d24ed 100644 --- a/modules/sor/sorV2/beetsHelpers.ts +++ b/modules/sor/sorV2/beetsHelpers.ts @@ -1,6 +1,7 @@ -import { BatchSwapStep, NATIVE_ADDRESS, SingleSwap, SwapKind, ZERO_ADDRESS } from '@balancer/sdk'; -import { GqlPoolMinimal, GqlSorSwapRoute, GqlSorSwapRouteHop } from '../../../schema'; +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, @@ -22,6 +23,25 @@ export function mapRoutes( 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, diff --git a/modules/sor/sorV2/lib/pools/stable/stablePool.ts b/modules/sor/sorV2/lib/pools/composableStable/composableStablePool.ts similarity index 90% rename from modules/sor/sorV2/lib/pools/stable/stablePool.ts rename to modules/sor/sorV2/lib/pools/composableStable/composableStablePool.ts index 83dce24a3..272457f92 100644 --- a/modules/sor/sorV2/lib/pools/stable/stablePool.ts +++ b/modules/sor/sorV2/lib/pools/composableStable/composableStablePool.ts @@ -14,8 +14,9 @@ import { import { BasePool, 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'; -export class StablePoolToken extends TokenAmount { +export class ComposableStablePoolToken extends TokenAmount { public readonly rate: bigint; public readonly index: number; @@ -39,23 +40,24 @@ export class StablePoolToken extends TokenAmount { } } -export class StablePool implements BasePool { +export class ComposableStablePool implements BasePool { public readonly chain: Chain; public readonly id: Hex; public readonly address: string; - public readonly poolType: PoolType = PoolType.MetaStable; + public readonly poolType: PoolType = PoolType.ComposableStable; public readonly amp: bigint; public readonly swapFee: bigint; public readonly bptIndex: number; + public readonly tokenPairs: TokenPairData[]; public totalShares: bigint; - public tokens: StablePoolToken[]; + public tokens: ComposableStablePoolToken[]; - private readonly tokenMap: Map; + private readonly tokenMap: Map; private readonly tokenIndexMap: Map; - static fromPrismaPool(pool: PrismaPoolWithDynamic): StablePool { - const poolTokens: StablePoolToken[] = []; + static fromPrismaPool(pool: PrismaPoolWithDynamic): ComposableStablePool { + const poolTokens: ComposableStablePoolToken[] = []; if (!pool.dynamicData) throw new Error('Stable pool has no dynamic data'); @@ -71,7 +73,7 @@ export class StablePool implements BasePool { const tokenAmount = TokenAmount.fromHumanAmount(token, `${parseFloat(poolToken.dynamicData.balance)}`); poolTokens.push( - new StablePoolToken( + new ComposableStablePoolToken( token, tokenAmount.amount, parseEther(poolToken.dynamicData.priceRate), @@ -83,7 +85,7 @@ export class StablePool implements BasePool { const totalShares = parseEther(pool.dynamicData.totalShares); const amp = parseUnits((pool.typeData as StableData).amp, 3); - return new StablePool( + return new ComposableStablePool( pool.id as Hex, pool.address, pool.chain, @@ -91,6 +93,7 @@ export class StablePool implements BasePool { parseEther(pool.dynamicData.swapFee), poolTokens, totalShares, + pool.dynamicData.tokenPairsData as TokenPairData[], ); } @@ -100,8 +103,9 @@ export class StablePool implements BasePool { chain: Chain, amp: bigint, swapFee: bigint, - tokens: StablePoolToken[], + tokens: ComposableStablePoolToken[], totalShares: bigint, + tokenPairs: TokenPairData[], ) { this.chain = chain; this.id = id; @@ -115,6 +119,7 @@ export class StablePool implements BasePool { this.tokenIndexMap = new Map(this.tokens.map((token) => [token.token.address, token.index])); this.bptIndex = this.tokens.findIndex((t) => t.token.address === this.address); + this.tokenPairs = tokenPairs; } public getNormalizedLiquidity(tokenIn: Token, tokenOut: Token): bigint { @@ -122,8 +127,17 @@ export class StablePool implements BasePool { const tOut = this.tokenMap.get(tokenOut.wrapped); if (!tIn || !tOut) throw new Error('Pool does not contain the tokens provided'); - // TODO: Fix stable normalized liquidity calc - return tOut.amount * this.amp; + + const tokenPair = this.tokenPairs.find( + (tokenPair) => + (tokenPair.tokenA === tIn.token.address && tokenPair.tokenB === tOut.token.address) || + (tokenPair.tokenA === tOut.token.address && tokenPair.tokenB === tIn.token.address), + ); + + if (tokenPair) { + return parseEther(tokenPair.normalizedLiquidity); + } + return 0n; } public swapGivenIn( diff --git a/modules/sor/sorV2/lib/pools/stable/stableMath.ts b/modules/sor/sorV2/lib/pools/composableStable/stableMath.ts similarity index 100% rename from modules/sor/sorV2/lib/pools/stable/stableMath.ts rename to modules/sor/sorV2/lib/pools/composableStable/stableMath.ts diff --git a/modules/sor/sorV2/lib/pools/fx/fxPool.ts b/modules/sor/sorV2/lib/pools/fx/fxPool.ts index 962885078..a0089458a 100644 --- a/modules/sor/sorV2/lib/pools/fx/fxPool.ts +++ b/modules/sor/sorV2/lib/pools/fx/fxPool.ts @@ -9,6 +9,7 @@ import { RAY } from '../../utils/math'; import { FxPoolPairData } from './types'; import { BasePool, PoolType, SwapKind, Token, TokenAmount } from '@balancer/sdk'; import { chainToIdMap } from '../../../../../network/network-config'; +import { TokenPairData } from '../../../../../pool/lib/pool-on-chain-tokenpair-data'; const isUSDC = (address: string): boolean => { return ( @@ -30,6 +31,7 @@ export class FxPool implements BasePool { public readonly delta: bigint; public readonly epsilon: bigint; public readonly tokens: FxPoolToken[]; + public readonly tokenPairs: TokenPairData[]; private readonly tokenMap: Map; @@ -79,6 +81,7 @@ export class FxPool implements BasePool { parseUnits((pool.typeData as FxData).delta as string, 36), parseFixedCurveParam((pool.typeData as FxData).epsilon as string), poolTokens, + pool.dynamicData.tokenPairsData as TokenPairData[], ); } @@ -94,6 +97,7 @@ export class FxPool implements BasePool { delta: bigint, epsilon: bigint, tokens: FxPoolToken[], + tokenPairs: TokenPairData[], ) { this.id = id; this.address = address; @@ -107,6 +111,7 @@ export class FxPool implements BasePool { this.epsilon = epsilon; this.tokens = tokens; this.tokenMap = new Map(this.tokens.map((token) => [token.token.address, token])); + this.tokenPairs = tokenPairs; } public getNormalizedLiquidity(tokenIn: Token, tokenOut: Token): bigint { @@ -114,8 +119,17 @@ export class FxPool implements BasePool { const tOut = this.tokenMap.get(tokenOut.wrapped); if (!tIn || !tOut) throw new Error('Pool does not contain the tokens provided'); - // TODO: Fix fx normalized liquidity calc - return tOut.amount; + + const tokenPair = this.tokenPairs.find( + (tokenPair) => + (tokenPair.tokenA === tIn.token.address && tokenPair.tokenB === tOut.token.address) || + (tokenPair.tokenA === tOut.token.address && tokenPair.tokenB === tIn.token.address), + ); + + if (tokenPair) { + return parseEther(tokenPair.normalizedLiquidity); + } + return 0n; } public swapGivenIn( diff --git a/modules/sor/sorV2/lib/pools/gyro2/gyro2Pool.ts b/modules/sor/sorV2/lib/pools/gyro2/gyro2Pool.ts index 5be64bbda..3de14d818 100644 --- a/modules/sor/sorV2/lib/pools/gyro2/gyro2Pool.ts +++ b/modules/sor/sorV2/lib/pools/gyro2/gyro2Pool.ts @@ -7,6 +7,7 @@ import { SWAP_LIMIT_FACTOR } from '../../utils/gyroHelpers/math'; import { BasePool, 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'; export class Gyro2PoolToken extends TokenAmount { public readonly index: number; @@ -37,6 +38,7 @@ export class Gyro2Pool implements BasePool { public readonly poolTypeVersion: number; public readonly swapFee: bigint; public readonly tokens: Gyro2PoolToken[]; + public readonly tokenPairs: TokenPairData[]; private readonly sqrtAlpha: bigint; private readonly sqrtBeta: bigint; @@ -76,6 +78,7 @@ export class Gyro2Pool implements BasePool { parseEther(gyroData.sqrtAlpha!), parseEther(gyroData.sqrtBeta!), poolTokens, + pool.dynamicData.tokenPairsData as TokenPairData[], ); } @@ -88,6 +91,7 @@ export class Gyro2Pool implements BasePool { sqrtAlpha: bigint, sqrtBeta: bigint, tokens: Gyro2PoolToken[], + tokenPairs: TokenPairData[], ) { this.id = id; this.address = address; @@ -98,6 +102,7 @@ export class Gyro2Pool implements BasePool { this.sqrtBeta = sqrtBeta; this.tokens = tokens; this.tokenMap = new Map(this.tokens.map((token) => [token.token.address, token])); + this.tokenPairs = tokenPairs; } public getNormalizedLiquidity(tokenIn: Token, tokenOut: Token): bigint { @@ -105,8 +110,17 @@ export class Gyro2Pool implements BasePool { const tOut = this.tokenMap.get(tokenOut.wrapped); if (!tIn || !tOut) throw new Error('Pool does not contain the tokens provided'); - // TODO: Fix gyro normalized liquidity calc - return tOut.amount; + + const tokenPair = this.tokenPairs.find( + (tokenPair) => + (tokenPair.tokenA === tIn.token.address && tokenPair.tokenB === tOut.token.address) || + (tokenPair.tokenA === tOut.token.address && tokenPair.tokenB === tIn.token.address), + ); + + if (tokenPair) { + return parseEther(tokenPair.normalizedLiquidity); + } + return 0n; } public swapGivenIn( diff --git a/modules/sor/sorV2/lib/pools/gyro3/gyro3Pool.ts b/modules/sor/sorV2/lib/pools/gyro3/gyro3Pool.ts index e5f72380e..1ab04963a 100644 --- a/modules/sor/sorV2/lib/pools/gyro3/gyro3Pool.ts +++ b/modules/sor/sorV2/lib/pools/gyro3/gyro3Pool.ts @@ -7,6 +7,7 @@ import { _calcInGivenOut, _calcOutGivenIn, _calculateInvariant } from './gyro3Ma import { BasePool, 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'; export class Gyro3PoolToken extends TokenAmount { public readonly index: number; @@ -37,6 +38,7 @@ export class Gyro3Pool implements BasePool { public readonly poolTypeVersion: number; public readonly swapFee: bigint; public readonly tokens: Gyro3PoolToken[]; + public readonly tokenPairs: TokenPairData[]; private readonly root3Alpha: bigint; private readonly tokenMap: Map; @@ -72,6 +74,7 @@ export class Gyro3Pool implements BasePool { parseEther(pool.dynamicData.swapFee), parseEther((pool.typeData as GyroData).root3Alpha!), poolTokens, + pool.dynamicData.tokenPairsData as TokenPairData[], ); } constructor( @@ -82,6 +85,7 @@ export class Gyro3Pool implements BasePool { swapFee: bigint, root3Alpha: bigint, tokens: Gyro3PoolToken[], + tokenPairs: TokenPairData[], ) { this.id = id; this.address = address; @@ -91,6 +95,7 @@ export class Gyro3Pool implements BasePool { this.root3Alpha = root3Alpha; this.tokens = tokens; this.tokenMap = new Map(this.tokens.map((token) => [token.token.address, token])); + this.tokenPairs = tokenPairs; } public getNormalizedLiquidity(tokenIn: Token, tokenOut: Token): bigint { @@ -98,8 +103,17 @@ export class Gyro3Pool implements BasePool { const tOut = this.tokenMap.get(tokenOut.wrapped); if (!tIn || !tOut) throw new Error('Pool does not contain the tokens provided'); - // TODO: Fix gyro normalized liquidity calc - return tOut.amount; + + const tokenPair = this.tokenPairs.find( + (tokenPair) => + (tokenPair.tokenA === tIn.token.address && tokenPair.tokenB === tOut.token.address) || + (tokenPair.tokenA === tOut.token.address && tokenPair.tokenB === tIn.token.address), + ); + + if (tokenPair) { + return parseEther(tokenPair.normalizedLiquidity); + } + return 0n; } public swapGivenIn( diff --git a/modules/sor/sorV2/lib/pools/gyroE/gyroEPool.ts b/modules/sor/sorV2/lib/pools/gyroE/gyroEPool.ts index 58bf36c0a..81dc4086d 100644 --- a/modules/sor/sorV2/lib/pools/gyroE/gyroEPool.ts +++ b/modules/sor/sorV2/lib/pools/gyroE/gyroEPool.ts @@ -9,6 +9,7 @@ import { calculateInvariantWithError, calcOutGivenIn, calcInGivenOut } from './g import { BasePool, 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'; export class GyroEPoolToken extends TokenAmount { public readonly rate: bigint; @@ -44,6 +45,7 @@ export class GyroEPool implements BasePool { public readonly tokens: GyroEPoolToken[]; public readonly gyroEParams: GyroEParams; public readonly derivedGyroEParams: DerivedGyroEParams; + public readonly tokenPairs: TokenPairData[]; private readonly tokenMap: Map; @@ -107,6 +109,7 @@ export class GyroEPool implements BasePool { poolTokens, gyroEParams, derivedGyroEParams, + pool.dynamicData.tokenPairsData as TokenPairData[], ); } @@ -119,6 +122,7 @@ export class GyroEPool implements BasePool { tokens: GyroEPoolToken[], gyroEParams: GyroEParams, derivedGyroEParams: DerivedGyroEParams, + tokenPairs: TokenPairData[], ) { this.id = id; this.address = address; @@ -129,6 +133,7 @@ export class GyroEPool implements BasePool { this.tokenMap = new Map(this.tokens.map((token) => [token.token.address, token])); this.gyroEParams = gyroEParams; this.derivedGyroEParams = derivedGyroEParams; + this.tokenPairs = tokenPairs; } public getNormalizedLiquidity(tokenIn: Token, tokenOut: Token): bigint { @@ -136,8 +141,17 @@ export class GyroEPool implements BasePool { const tOut = this.tokenMap.get(tokenOut.wrapped); if (!tIn || !tOut) throw new Error('Pool does not contain the tokens provided'); - // TODO: Fix gyro normalized liquidity calc - return tOut.amount; + + const tokenPair = this.tokenPairs.find( + (tokenPair) => + (tokenPair.tokenA === tIn.token.address && tokenPair.tokenB === tOut.token.address) || + (tokenPair.tokenA === tOut.token.address && tokenPair.tokenB === tIn.token.address), + ); + + if (tokenPair) { + return parseEther(tokenPair.normalizedLiquidity); + } + return 0n; } public swapGivenIn( diff --git a/modules/sor/sorV2/lib/pools/metastable/metastablePool.ts b/modules/sor/sorV2/lib/pools/metastable/metastablePool.ts index d0479f5ee..281f04a2e 100644 --- a/modules/sor/sorV2/lib/pools/metastable/metastablePool.ts +++ b/modules/sor/sorV2/lib/pools/metastable/metastablePool.ts @@ -1,12 +1,13 @@ import { Chain } from '@prisma/client'; import { Address, Hex, parseEther, parseUnits } from 'viem'; -import { StablePoolToken } from '../stable/stablePool'; +import { ComposableStablePoolToken } from '../composableStable/composableStablePool'; import { PrismaPoolWithDynamic } from '../../../../../../prisma/prisma-types'; -import { _calcInGivenOut, _calcOutGivenIn, _calculateInvariant } from '../stable/stableMath'; +import { _calcInGivenOut, _calcOutGivenIn, _calculateInvariant } from '../composableStable/stableMath'; import { MathSol, WAD } from '../../utils/math'; import { BasePool, 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'; export class MetaStablePool implements BasePool { public readonly chain: Chain; @@ -15,13 +16,14 @@ export class MetaStablePool implements BasePool { public readonly poolType: PoolType = PoolType.MetaStable; public readonly amp: bigint; public readonly swapFee: bigint; - public readonly tokens: StablePoolToken[]; + public readonly tokens: ComposableStablePoolToken[]; + public readonly tokenPairs: TokenPairData[]; - private readonly tokenMap: Map; + private readonly tokenMap: Map; private readonly tokenIndexMap: Map; static fromPrismaPool(pool: PrismaPoolWithDynamic): MetaStablePool { - const poolTokens: StablePoolToken[] = []; + const poolTokens: ComposableStablePoolToken[] = []; if (!pool.dynamicData) throw new Error('Stable pool has no dynamic data'); @@ -37,7 +39,7 @@ export class MetaStablePool implements BasePool { const tokenAmount = TokenAmount.fromHumanAmount(token, `${parseFloat(poolToken.dynamicData.balance)}`); poolTokens.push( - new StablePoolToken( + new ComposableStablePoolToken( token, tokenAmount.amount, parseEther(poolToken.dynamicData.priceRate), @@ -55,10 +57,19 @@ export class MetaStablePool implements BasePool { amp, parseEther(pool.dynamicData.swapFee), poolTokens, + pool.dynamicData.tokenPairsData as TokenPairData[], ); } - constructor(id: Hex, address: string, chain: Chain, amp: bigint, swapFee: bigint, tokens: StablePoolToken[]) { + constructor( + id: Hex, + address: string, + chain: Chain, + amp: bigint, + swapFee: bigint, + tokens: ComposableStablePoolToken[], + tokenPairs: TokenPairData[], + ) { this.id = id; this.address = address; this.chain = chain; @@ -68,6 +79,7 @@ export class MetaStablePool implements BasePool { this.tokens = tokens.sort((a, b) => a.index - b.index); this.tokenMap = new Map(this.tokens.map((token) => [token.token.address, token])); this.tokenIndexMap = new Map(this.tokens.map((token) => [token.token.address, token.index])); + this.tokenPairs = tokenPairs; } public getNormalizedLiquidity(tokenIn: Token, tokenOut: Token): bigint { @@ -75,8 +87,17 @@ export class MetaStablePool implements BasePool { const tOut = this.tokenMap.get(tokenOut.address); if (!tIn || !tOut) throw new Error('Pool does not contain the tokens provided'); - // TODO: Fix stable normalized liquidity calc - return tOut.amount * this.amp; + + const tokenPair = this.tokenPairs.find( + (tokenPair) => + (tokenPair.tokenA === tIn.token.address && tokenPair.tokenB === tOut.token.address) || + (tokenPair.tokenA === tOut.token.address && tokenPair.tokenB === tIn.token.address), + ); + + if (tokenPair) { + return parseEther(tokenPair.normalizedLiquidity); + } + return 0n; } public swapGivenIn( diff --git a/modules/sor/sorV2/lib/pools/weighted/weightedPool.ts b/modules/sor/sorV2/lib/pools/weighted/weightedPool.ts index d06fbdacf..d2965bcbf 100644 --- a/modules/sor/sorV2/lib/pools/weighted/weightedPool.ts +++ b/modules/sor/sorV2/lib/pools/weighted/weightedPool.ts @@ -5,6 +5,7 @@ import { MathSol, WAD } from '../../utils/math'; import { Address, Hex, parseEther } from 'viem'; import { BasePool, BigintIsh, SwapKind, Token, TokenAmount } from '@balancer/sdk'; import { chainToIdMap } from '../../../../../network/network-config'; +import { TokenPairData } from '../../../../../pool/lib/pool-on-chain-tokenpair-data'; export class WeightedPoolToken extends TokenAmount { public readonly weight: bigint; @@ -37,6 +38,7 @@ export class WeightedPool implements BasePool { public readonly poolTypeVersion: number; public readonly swapFee: bigint; public readonly tokens: WeightedPoolToken[]; + public readonly tokenPairs: TokenPairData[]; private readonly tokenMap: Map; private readonly MAX_IN_RATIO = 300000000000000000n; // 0.3 @@ -80,6 +82,7 @@ export class WeightedPool implements BasePool { pool.version, parseEther(pool.dynamicData.swapFee), poolTokens, + pool.dynamicData.tokenPairsData as TokenPairData[], ); } @@ -90,6 +93,7 @@ export class WeightedPool implements BasePool { poolTypeVersion: number, swapFee: bigint, tokens: WeightedPoolToken[], + tokenPairs: TokenPairData[], ) { this.chain = chain; this.id = id; @@ -98,12 +102,22 @@ export class WeightedPool implements BasePool { this.swapFee = swapFee; this.tokens = tokens; this.tokenMap = new Map(tokens.map((token) => [token.token.address, token])); + this.tokenPairs = tokenPairs; } public getNormalizedLiquidity(tokenIn: Token, tokenOut: Token): bigint { const { tIn, tOut } = this.getRequiredTokenPair(tokenIn, tokenOut); - return (tIn.amount * tOut.weight) / (tIn.weight + tOut.weight); + const tokenPair = this.tokenPairs.find( + (tokenPair) => + (tokenPair.tokenA === tIn.token.address && tokenPair.tokenB === tOut.token.address) || + (tokenPair.tokenA === tOut.token.address && tokenPair.tokenB === tIn.token.address), + ); + + if (tokenPair) { + return parseEther(tokenPair.normalizedLiquidity); + } + return 0n; } public getLimitAmountSwap(tokenIn: Token, tokenOut: Token, swapKind: SwapKind): bigint { diff --git a/modules/sor/sorV2/lib/static.ts b/modules/sor/sorV2/lib/static.ts index 5817fbc93..ccb0f99a0 100644 --- a/modules/sor/sorV2/lib/static.ts +++ b/modules/sor/sorV2/lib/static.ts @@ -2,13 +2,13 @@ import { Router } from './router'; import { PrismaPoolWithDynamic } from '../../../../prisma/prisma-types'; import { checkInputs } from './utils/helpers'; import { WeightedPool } from './pools/weighted/weightedPool'; -import { StablePool } from './pools/stable/stablePool'; import { MetaStablePool } from './pools/metastable/metastablePool'; 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 { ComposableStablePool } from './pools/composableStable/composableStablePool'; export async function sorGetSwapsWithPools( tokenIn: Token, @@ -29,7 +29,7 @@ export async function sorGetSwapsWithPools( break; case 'COMPOSABLE_STABLE': case 'PHANTOM_STABLE': - basePools.push(StablePool.fromPrismaPool(prismaPool)); + basePools.push(ComposableStablePool.fromPrismaPool(prismaPool)); break; case 'META_STABLE': basePools.push(MetaStablePool.fromPrismaPool(prismaPool)); diff --git a/modules/sor/sorV2/sorV2.service.ts b/modules/sor/sorV2/sorV2.service.ts index efb202965..3219ae950 100644 --- a/modules/sor/sorV2/sorV2.service.ts +++ b/modules/sor/sorV2/sorV2.service.ts @@ -1,8 +1,8 @@ -import { GqlSorGetSwaps, GqlSorSwap, GqlSorSwapType } from '../../../schema'; +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, SwapResult, SwapService } from '../types'; +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'; @@ -12,10 +12,11 @@ import { Address, formatUnits, parseUnits } from 'viem'; import { sorGetSwapsWithPools } from './lib/static'; import { SwapResultV2 } from './swapResultV2'; import { poolService } from '../../pool/pool.service'; -import { mapRoutes } from './beetsHelpers'; +import { mapPaths, mapRoutes } from './beetsHelpers'; import { replaceZeroAddressWithEth } from '../../web3/addresses'; -import { getToken, zeroResponseV2 } from '../utils'; +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( @@ -65,20 +66,16 @@ export class SorV2Service implements SwapService { } } - public async getSorSwaps(input: GetSwapsInput, maxNonBoostedPathDepth = 4): Promise { + public async getSorSwapPaths(input: GetSwapPathsInput, maxNonBoostedPathDepth = 4): Promise { const swap = await this.getSwap(input, maxNonBoostedPathDepth); - const emptyResponse = zeroResponseV2(input.swapType, input.tokenIn, input.tokenOut); + const emptyResponse = swapPathsZeroResponse(input.tokenIn, input.tokenOut); if (!swap) { return emptyResponse; } try { - return this.mapToSorSwaps( - swap!, - input.chain, - input.swapOptions.queryBatchSwap ? input.swapOptions.queryBatchSwap : false, - ); + return this.mapToSorSwapPaths(swap!, input.chain, input.queryBatchSwap); } catch (err: any) { console.log(`Error Retrieving QuerySwap`, err); Sentry.captureException(err.message, { @@ -95,8 +92,8 @@ export class SorV2Service implements SwapService { } } - public async getSwap( - { chain, tokenIn, tokenOut, swapType, swapAmount, graphTraversalConfig }: GetSwapsInput, + private async getSwap( + { chain, tokenIn, tokenOut, swapType, swapAmount, graphTraversalConfig }: GetSwapPathsInput, maxNonBoostedPathDepth = 4, ): Promise { try { @@ -117,8 +114,9 @@ export class SorV2Service implements SwapService { }, }; 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 swap; + return this.getSwap(arguments[0], maxNonBoostedPathDepth + 1); } return swap; } catch (err: any) { @@ -139,12 +137,10 @@ export class SorV2Service implements SwapService { } } - public async mapToSorSwaps(swap: Swap, chain: Chain, queryFirst = false): Promise { + 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 balancerQueriesAddress = AllNetworkConfigsKeyedOnChain[chain].data.balancer.v2.balancerQueriesAddress; - // const updatedResult = await swap.query(rpcUrl, balancerQueriesAddress as Address); const updatedResult = await swap.query(rpcUrl); const inputAmount = swap.swapKind === SwapKind.GivenIn ? swap.inputAmount : updatedResult; @@ -158,7 +154,7 @@ export class SorV2Service implements SwapService { swap: Swap, inputAmount: TokenAmount, outputAmount: TokenAmount, - ): Promise { + ): Promise { const priceImpact = swap.priceImpact.decimal.toFixed(4); let poolIds: string[]; if (swap.isBatchSwap) { @@ -192,6 +188,8 @@ export class SorV2Service implements SwapService { swap.swapKind, ); + const paths = mapPaths(swap); + for (const route of routes) { route.tokenInAmount = ((inputAmount.amount * BigInt(parseUnits(`${0.5}`, 6))) / 1000000n).toString(); route.tokenOutAmount = ( @@ -201,11 +199,11 @@ export class SorV2Service implements SwapService { } return { + vaultVersion: 2, + paths: paths, swaps: this.mapSwaps(swap.swaps, swap.assets), - tokenAddresses: swap.assets, tokenIn: replaceZeroAddressWithEth(inputAmount.token.address), tokenOut: replaceZeroAddressWithEth(outputAmount.token.address), - swapType: this.mapSwapKindToSwapType(swap.swapKind), tokenInAmount: inputAmount.amount.toString(), tokenOutAmount: outputAmount.amount.toString(), swapAmount: formatUnits(swapAmount.amount, swapAmount.token.decimals), @@ -229,10 +227,6 @@ export class SorV2Service implements SwapService { return swapType === 'EXACT_IN' ? SwapKind.GivenIn : SwapKind.GivenOut; } - private mapSwapKindToSwapType(kind: SwapKind): GqlSorSwapType { - return kind === SwapKind.GivenIn ? 'EXACT_IN' : 'EXACT_OUT'; - } - private mapSwaps(swaps: BatchSwapStep[] | SingleSwap, assets: string[]): GqlSorSwap[] { if (Array.isArray(swaps)) { return swaps.map((swap) => { diff --git a/modules/sor/types.ts b/modules/sor/types.ts index 71248e7ef..d0487fab6 100644 --- a/modules/sor/types.ts +++ b/modules/sor/types.ts @@ -11,6 +11,16 @@ export interface GetSwapsInput { graphTraversalConfig?: GraphTraversalConfig; } +export interface GetSwapsV2Input { + chain: Chain; + tokenIn: string; + tokenOut: string; + swapType: GqlSorSwapType; + swapAmount: TokenAmount; + queryBatchSwap: boolean; + graphTraversalConfig?: GraphTraversalConfig; +} + export interface GraphTraversalConfig { approxPathsToReturn?: number; maxDepth?: number; diff --git a/modules/sor/utils.ts b/modules/sor/utils.ts index bfc42cd91..40bbeb455 100644 --- a/modules/sor/utils.ts +++ b/modules/sor/utils.ts @@ -1,7 +1,7 @@ import { tokenService } from '../token/token.service'; import { Chain } from '@prisma/client'; import { AllNetworkConfigsKeyedOnChain, chainToIdMap } from '../network/network-config'; -import { GqlSorGetSwaps, GqlSorGetSwapsResponse, GqlSorSwapType } from '../../schema'; +import { GqlSorGetSwapPaths, GqlSorGetSwapsResponse, GqlSorSwapType } from '../../schema'; import { replaceZeroAddressWithEth } from '../web3/addresses'; import { Address } from 'viem'; import { NATIVE_ADDRESS, Token, TokenAmount } from '@balancer/sdk'; @@ -37,13 +37,13 @@ export const getToken = async (tokenAddr: string, chain: Chain): Promise } }; -export const zeroResponseV2 = (swapType: GqlSorSwapType, tokenIn: string, tokenOut: string): GqlSorGetSwaps => { +export const swapPathsZeroResponse = (tokenIn: string, tokenOut: string): GqlSorGetSwapPaths => { return { - tokenAddresses: [], swaps: [], + paths: [], + vaultVersion: 2, tokenIn: replaceZeroAddressWithEth(tokenIn), tokenOut: replaceZeroAddressWithEth(tokenOut), - swapType, tokenInAmount: '0', tokenOutAmount: '0', swapAmount: '0', diff --git a/prisma/migrations/20240209084438_add_tokenpair_data/migration.sql b/prisma/migrations/20240209084438_add_tokenpair_data/migration.sql new file mode 100644 index 000000000..da857ecdd --- /dev/null +++ b/prisma/migrations/20240209084438_add_tokenpair_data/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "PrismaPoolDynamicData" ADD COLUMN "tokenPairsData" JSONB NOT NULL DEFAULT '[]'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bf06b9fd0..acce1b090 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -162,6 +162,8 @@ model PrismaPoolDynamicData { fees24hAthTimestamp Int @default(0) fees24hAtl Float @default(0) fees24hAtlTimestamp Int @default(0) + + tokenPairsData Json @default("[]") } model PrismaPoolToken { diff --git a/stellate/api/stellate.js b/stellate/api/stellate.js new file mode 100644 index 000000000..9bdb0dd5a --- /dev/null +++ b/stellate/api/stellate.js @@ -0,0 +1,87 @@ +/** + * @type {import('stellate').Config} + */ +const config = { + config: { + schema: 'https://api-v3.balancer.fi/graphql', + enablePlayground: true, + queryDepthLimit: 10, + scopes: { + CHAIN: 'header:chainid', + AUTHENTICATED: 'header:accountaddress', + AUTHENTICATED_CHAIN: 'header:accountaddress|header:chainid', + }, + rootTypeNames: { + query: 'Query', + mutation: 'Mutation', + }, + rules: [ + { + types: ['Query'], + maxAge: 30, + swr: 60, + scope: 'CHAIN', + description: 'Cache everything (default)', + }, + + { + types: { + Query: [ + 'userGetSwaps', + 'userGetStaking', + 'userGetPoolBalances', + 'userGetFbeetsBalance', + 'userGetPoolJoinExits', + ], + }, + maxAge: 10, + swr: 15, + scope: 'AUTHENTICATED_CHAIN', + description: 'Time critical user queries', + }, + { + types: { + Query: ['latestSyncedBlocks'], + }, + maxAge: 2, + swr: 10, + scope: 'CHAIN', + description: 'Time critical block data', + }, + { + types: { + Query: [ + 'tokenGetTokens', + 'contentGetNewsItems', + 'protocolMetricsChain', + 'blocksGetBlocksPerDay', + 'blocksGetBlocksPerSecond', + 'blocksGetAverageBlockTime', + 'poolGetFeaturedPoolGroups', + 'protocolMetricsAggregated', + ], + }, + maxAge: 60, + swr: 120, + scope: 'CHAIN', + description: 'Mostly static, cache for a long time', + }, + ], + name: 'api-v3', + originUrl: 'https://api-v3-origin.balancer.fi/graphql', + getConsumerIdentifiers: + "(() => {\n function getIp(req) {\n const { ip } = req;\n // const allowedIps = []\n // if (allowedIps.includes(ip)) {\n // return null\n // }\n if (req.headers['X-Forwarded-For']) {\n return req.headers['X-Forwarded-For'].split(',')[0];\n }\n return ip;\n}\n return (req) => ({\n ip: getIp(req),\n})\n})()", + rateLimit: { + name: 'IP Limit', + consumerIdentifier: 'ip', + allowList: [], + limit: { + type: 'QueryComplexity', + budget: 5000, + window: '5m', + }, + }, + }, +}; + +export default config; diff --git a/stellate/backend-canary/stellate.js b/stellate/backend-canary/stellate.js new file mode 100644 index 000000000..1461c07e6 --- /dev/null +++ b/stellate/backend-canary/stellate.js @@ -0,0 +1,81 @@ +/** + * @type {import('stellate').Config} + */ +const config = { + config: { + schema: 'https://backend-v3-canary-origin.beets-ftm-node.com/graphql', + queryDepthLimit: 10, + scopes: { + CHAIN: 'header:chainid', + AUTHENTICATED: 'header:accountaddress', + AUTHENTICATED_CHAIN: 'header:accountaddress|header:chainid', + }, + rootTypeNames: { + query: 'Query', + mutation: 'Mutation', + }, + rules: [ + { + types: ['Query'], + maxAge: 15, + swr: 30, + scope: 'CHAIN', + description: 'Cache everything (default)', + }, + { + types: { + Query: [ + 'userGetSwaps', + 'userGetStaking', + 'userGetPoolBalances', + 'userGetFbeetsBalance', + 'userGetPoolJoinExits', + ], + }, + maxAge: 10, + swr: 15, + scope: 'AUTHENTICATED_CHAIN', + description: 'Time critical user queries', + }, + { + types: { + Query: ['latestSyncedBlocks'], + }, + maxAge: 2, + swr: 10, + scope: 'CHAIN', + description: 'Time critical block data', + }, + { + types: { + Query: [ + 'tokenGetTokens', + 'beetsGetFbeetsRatio', + 'contentGetNewsItems', + 'protocolMetricsChain', + 'blocksGetBlocksPerDay', + 'blocksGetBlocksPerSecond', + 'blocksGetAverageBlockTime', + 'poolGetFeaturedPoolGroups', + 'protocolMetricsAggregated', + 'tokenGetProtocolTokenPrice', + ], + }, + maxAge: 60, + swr: 120, + scope: 'CHAIN', + description: 'Mostly static, cache for a long time', + }, + ], + name: 'backend-v3-canary', + originUrl: 'https://backend-v3-canary-origin.beets-ftm-node.com/graphql', + devPortal: { + enabled: false, + auth: false, + }, + rateLimits: + "(req) => {\n if (req.headers['stellate-api-token'] &&\n req.headers['stellate-api-token'] ===\n 'stl8_bcebb2b60910a55e58a82c8e83825034dc763e294582447118fab0a6a1225ebb') {\n return [\n {\n name: 'Specific API Token based limits',\n state: 'dryRun',\n group: req.headers['stellate-api-token'],\n limit: {\n type: 'RequestCount',\n budget: 20,\n window: '1m',\n },\n },\n ];\n }\n if (req.headers['stellate-api-token']) {\n return [\n {\n name: 'General API Token based limits',\n state: 'dryRun',\n group: req.headers['stellate-api-token'],\n limit: {\n type: 'RequestCount',\n budget: 10,\n window: '1m',\n },\n },\n ];\n }\n const xForwardedFor = Array.isArray(req.headers['x-forwarded-for'])\n ? req.headers['x-forwarded-for'][0]\n : req.headers['x-forwarded-for'];\n return [\n {\n name: 'IP based limits',\n state: 'dryRun',\n group: xForwardedFor ? xForwardedFor.split(',')[0] : req.ip,\n limit: {\n type: 'RequestCount',\n budget: 5,\n window: '1m',\n },\n },\n ];\n}", + }, +}; + +export default config; diff --git a/stellate/backend/stellate.js b/stellate/backend/stellate.js new file mode 100644 index 000000000..76da9ffe2 --- /dev/null +++ b/stellate/backend/stellate.js @@ -0,0 +1,87 @@ +/** + * @type {import('stellate').Config} + */ +const config = { + config: { + schema: 'https://backend-v3.beets-ftm-node.com/graphql', + queryDepthLimit: 10, + scopes: { + CHAIN: 'header:chainid', + AUTHENTICATED: 'header:accountaddress', + AUTHENTICATED_CHAIN: 'header:accountaddress|header:chainid', + }, + rootTypeNames: { + query: 'Query', + mutation: 'Mutation', + }, + rules: [ + { + types: ['Query'], + maxAge: 15, + swr: 30, + scope: 'CHAIN', + description: 'Cache everything (default)', + }, + { + types: { + Query: [ + 'userGetSwaps', + 'userGetStaking', + 'userGetPoolBalances', + 'userGetFbeetsBalance', + 'userGetPoolJoinExits', + ], + }, + maxAge: 10, + swr: 15, + scope: 'AUTHENTICATED_CHAIN', + description: 'Time critical user queries', + }, + { + types: { + Query: ['latestSyncedBlocks'], + }, + maxAge: 2, + swr: 10, + scope: 'CHAIN', + description: 'Time critical block data', + }, + { + types: { + Query: [ + 'tokenGetTokens', + 'beetsGetFbeetsRatio', + 'contentGetNewsItems', + 'protocolMetricsChain', + 'blocksGetBlocksPerDay', + 'blocksGetBlocksPerSecond', + 'blocksGetAverageBlockTime', + 'poolGetFeaturedPoolGroups', + 'protocolMetricsAggregated', + 'tokenGetProtocolTokenPrice', + ], + }, + maxAge: 60, + swr: 120, + scope: 'CHAIN', + description: 'Mostly static, cache for a long time', + }, + ], + name: 'backend-v3', + originUrl: 'https://backend-v3-origin.beets-ftm-node.com/graphql', + getConsumerIdentifiers: + "(() => {\n function getIp(req) {\n const { ip } = req;\n // const allowedIps = []\n // if (allowedIps.includes(ip)) {\n // return null\n // }\n if (req.headers['X-Forwarded-For']) {\n return req.headers['X-Forwarded-For'].split(',')[0];\n }\n return ip;\n}\n return (req) => ({\n ip: getIp(req),\n})\n})()", + rateLimit: { + name: 'IP Limit', + consumerIdentifier: 'ip', + allowList: [], + limit: { + type: 'QueryComplexity', + budget: 5000, + window: '5m', + }, + }, + }, +}; + +export default config; diff --git a/stellate/test-api/stellate.js b/stellate/test-api/stellate.js new file mode 100644 index 000000000..2914d671e --- /dev/null +++ b/stellate/test-api/stellate.js @@ -0,0 +1,86 @@ +/** + * @type {import('stellate').Config} + */ +const config = { + config: { + schema: 'https://test-api-v3.balancer.fi/graphql', + enablePlayground: true, + queryDepthLimit: 10, + scopes: { + CHAIN: 'header:chainid', + AUTHENTICATED: 'header:accountaddress', + AUTHENTICATED_CHAIN: 'header:accountaddress|header:chainid', + }, + rootTypeNames: { + query: 'Query', + mutation: 'Mutation', + }, + rules: [ + { + types: ['Query'], + maxAge: 15, + swr: 30, + scope: 'CHAIN', + description: 'Cache everything (default)', + }, + { + types: { + Query: [ + 'userGetSwaps', + 'userGetStaking', + 'userGetPoolBalances', + 'userGetFbeetsBalance', + 'userGetPoolJoinExits', + ], + }, + maxAge: 10, + swr: 15, + scope: 'AUTHENTICATED_CHAIN', + description: 'Time critical user queries', + }, + { + types: { + Query: ['latestSyncedBlocks'], + }, + maxAge: 2, + swr: 10, + scope: 'CHAIN', + description: 'Time critical block data', + }, + { + types: { + Query: [ + 'tokenGetTokens', + 'contentGetNewsItems', + 'protocolMetricsChain', + 'blocksGetBlocksPerDay', + 'blocksGetBlocksPerSecond', + 'blocksGetAverageBlockTime', + 'poolGetFeaturedPoolGroups', + 'protocolMetricsAggregated', + ], + }, + maxAge: 60, + swr: 120, + scope: 'CHAIN', + description: 'Mostly static, cache for a long time', + }, + ], + name: 'test-api-v3', + originUrl: 'https://test-api-v3-origin.balancer.fi/graphql', + getConsumerIdentifiers: + "(() => {\n function getIp(req) {\n const { ip } = req;\n // const allowedIps = []\n // if (allowedIps.includes(ip)) {\n // return null\n // }\n if (req.headers['X-Forwarded-For']) {\n return req.headers['X-Forwarded-For'].split(',')[0];\n }\n return ip;\n}\n return (req) => ({\n ip: getIp(req),\n})\n})()", + rateLimit: { + name: 'IP Limit', + consumerIdentifier: 'ip', + allowList: [], + limit: { + type: 'QueryComplexity', + budget: 5000, + window: '5m', + }, + }, + }, +}; + +export default config;