diff --git a/.changeset/dry-worms-explode.md b/.changeset/dry-worms-explode.md new file mode 100644 index 000000000..4bbd290a3 --- /dev/null +++ b/.changeset/dry-worms-explode.md @@ -0,0 +1,5 @@ +--- +'backend': patch +--- + +add reward token to yb and nested apr diff --git a/.changeset/fresh-horses-agree.md b/.changeset/fresh-horses-agree.md new file mode 100644 index 000000000..2900c880d --- /dev/null +++ b/.changeset/fresh-horses-agree.md @@ -0,0 +1,5 @@ +--- +'backend': minor +--- + +adding reward token data to apr item diff --git a/.changeset/grumpy-weeks-switch.md b/.changeset/grumpy-weeks-switch.md new file mode 100644 index 000000000..5be1d7bd2 --- /dev/null +++ b/.changeset/grumpy-weeks-switch.md @@ -0,0 +1,5 @@ +--- +'backend': patch +--- + +adding new pool query specific for aggregator needs diff --git a/.changeset/healthy-oranges-cheer.md b/.changeset/healthy-oranges-cheer.md new file mode 100644 index 000000000..571e93919 --- /dev/null +++ b/.changeset/healthy-oranges-cheer.md @@ -0,0 +1,5 @@ +--- +'backend': patch +--- + +refactoring VotingGaugesRepository to use viem diff --git a/graphql_schema_generated.ts b/graphql_schema_generated.ts index bf7223359..c9b06656f 100644 --- a/graphql_schema_generated.ts +++ b/graphql_schema_generated.ts @@ -280,6 +280,178 @@ export const schema = gql` valueUSD: Float! } + type GqlPoolAggregator { + """ + The contract address of the pool. + """ + address: Bytes! + + """ + Data specific to gyro/fx pools + """ + alpha: String + + """ + Data specific to stable pools + """ + amp: BigInt + + """ + Data specific to gyro/fx pools + """ + beta: String + + """ + Data specific to gyro pools + """ + c: String + + """ + The chain on which the pool is deployed + """ + chain: GqlChain! + + """ + The timestamp the pool was created. + """ + createTime: Int! + + """ + Data specific to gyro pools + """ + dSq: String + + """ + The decimals of the BPT, usually 18 + """ + decimals: Int! + + """ + Data specific to fx pools + """ + delta: String + + """ + Dynamic data such as token balances, swap fees or volume + """ + dynamicData: GqlPoolDynamicData! + + """ + Data specific to fx pools + """ + epsilon: String + + """ + The factory contract address from which the pool was created. + """ + factory: Bytes + + """ + The pool id. This is equal to the address for protocolVersion 3 pools + """ + id: ID! + + """ + Data specific to gyro/fx pools + """ + lambda: String + + """ + The name of the pool as per contract + """ + name: String! + + """ + The wallet address of the owner of the pool. Pool owners can set certain properties like swapFees or AMP. + """ + owner: Bytes + + """ + Returns all pool tokens, including BPTs and nested pools if there are any. Only one nested level deep. + """ + poolTokens: [GqlPoolTokenDetail!]! + + """ + The protocol version on which the pool is deployed, 1, 2 or 3 + """ + protocolVersion: Int! + + """ + Data specific to gyro pools + """ + root3Alpha: String + + """ + Data specific to gyro pools + """ + s: String + + """ + Data specific to gyro pools + """ + sqrtAlpha: String + + """ + Data specific to gyro pools + """ + sqrtBeta: String + + """ + The token symbol of the pool as per contract + """ + symbol: String! + + """ + Data specific to gyro pools + """ + tauAlphaX: String + + """ + Data specific to gyro pools + """ + tauAlphaY: String + + """ + Data specific to gyro pools + """ + tauBetaX: String + + """ + Data specific to gyro pools + """ + tauBetaY: String + + """ + The pool type, such as weighted, stable, etc. + """ + type: GqlPoolType! + + """ + Data specific to gyro pools + """ + u: String + + """ + Data specific to gyro pools + """ + v: String + + """ + The version of the pool type. + """ + version: Int! + + """ + Data specific to gyro pools + """ + w: String + + """ + Data specific to gyro pools + """ + z: String + } + type GqlPoolApr { apr: GqlPoolAprValue! hasRewardApr: Boolean! @@ -303,10 +475,20 @@ export const schema = gql` """ id: ID! + """ + The reward token address, if the APR originates from token emissions + """ + rewardTokenAddress: String + + """ + The reward token symbol, if the APR originates from token emissions + """ + rewardTokenSymbol: String + """ The title of the APR item, a human readable form """ - title: String! + title: String! @deprecated(reason: "No replacement, should be built client side") """ Specific type of this APR @@ -333,6 +515,11 @@ export const schema = gql` """ LOCKING + """ + Reward APR in a pool from maBEETS emissions allocated by gauge votes. Emitted in BEETS. + """ + MABEETS_EMISSIONS + """ Rewards distributed by merkl.xyz """ @@ -344,7 +531,7 @@ export const schema = gql` NESTED """ - Staking reward APR in a pool, i.e. BAL or BEETS. + Staking reward APR in a pool from a reward token. """ STAKING @@ -363,6 +550,11 @@ export const schema = gql` """ SWAP_FEE + """ + Reward APR in a pool from veBAL emissions allocated by gauge votes. Emitted in BAL. + """ + VEBAL_EMISSIONS + """ APR that can be earned thourgh voting, i.e. gauge votes """ @@ -2966,6 +3158,17 @@ export const schema = gql` """ poolEvents(first: Int, skip: Int, where: GqlPoolEventsFilter): [GqlPoolEvent!]! + """ + Returns all pools for a given filter, specific for aggregators + """ + poolGetAggregatorPools( + first: Int + orderBy: GqlPoolOrderBy + orderDirection: GqlPoolOrderDirection + skip: Int + where: GqlPoolFilter + ): [GqlPoolAggregator!]! + """ Will de deprecated in favor of poolEvents """ diff --git a/modules/actions/pool/staking/sync-gauge-staking.service.ts b/modules/actions/pool/staking/sync-gauge-staking.service.ts index 910d04017..8726d0574 100644 --- a/modules/actions/pool/staking/sync-gauge-staking.service.ts +++ b/modules/actions/pool/staking/sync-gauge-staking.service.ts @@ -157,7 +157,7 @@ export const syncGaugeStakingForPools = async ( } const dbStakingGauge = allDbStakingGauges.find((stakingGauge) => stakingGauge?.id === gauge.id); - const workingSupply = onchainRates.find(({ id }) => `${gauge.id}-${balAddress}-balgauge` === id)?.workingSupply; + const workingSupply = onchainRates.find(({ id }) => id.includes(gauge.id))?.workingSupply; const totalSupply = onchainRates.find(({ id }) => id.includes(gauge.id))?.totalSupply; if ( !dbStakingGauge || @@ -193,7 +193,7 @@ export const syncGaugeStakingForPools = async ( const allStakingGaugeRewards = allDbStakingGauges.map((gauge) => gauge?.rewards).flat(); // DB operations for gauge reward tokens - for (const { id, rewardPerSecond } of onchainRates) { + for (const { id, rewardPerSecond, isVeBalemissions } of onchainRates) { const [gaugeId, tokenAddress] = id.toLowerCase().split('-'); const token = prismaTokens.find((token) => token.address === tokenAddress); if (!token) { @@ -215,9 +215,11 @@ export const syncGaugeStakingForPools = async ( gaugeId, tokenAddress, rewardPerSecond, + isVeBalemissions, }, update: { rewardPerSecond, + isVeBalemissions, }, where: { id_chain: { id, chain: networkContext.chain } }, }), @@ -240,6 +242,7 @@ const getOnchainRewardTokensData = async ( rewardPerSecond: string; workingSupply: string; totalSupply: string; + isVeBalemissions: boolean; }[] > => { // Get onchain data for BAL rewards @@ -310,6 +313,7 @@ const getOnchainRewardTokensData = async ( rewardPerSecond, workingSupply: workingSupply ? formatUnits(workingSupply) : '0', totalSupply: totalSupply ? formatUnits(totalSupply) : '0', + isVeBalemissions: true, }; }), ...Object.keys(rewardsData) @@ -329,6 +333,7 @@ const getOnchainRewardTokensData = async ( rewardPerSecond, workingSupply: '0', totalSupply: totalSupply ? formatUnits(totalSupply) : '0', + isVeBalemissions: false, }; }), ]) @@ -338,6 +343,7 @@ const getOnchainRewardTokensData = async ( rewardPerSecond: string; workingSupply: string; totalSupply: string; + isVeBalemissions: boolean; }[]; return onchainRates; diff --git a/modules/pool/lib/apr-data-sources/fantom/masterchef-farm-apr.service.ts b/modules/pool/lib/apr-data-sources/fantom/masterchef-farm-apr.service.ts index 059b1a228..aef4bfe2b 100644 --- a/modules/pool/lib/apr-data-sources/fantom/masterchef-farm-apr.service.ts +++ b/modules/pool/lib/apr-data-sources/fantom/masterchef-farm-apr.service.ts @@ -110,6 +110,8 @@ export class MasterchefFarmAprService implements PoolAprService { apr: beetsApr, type: 'NATIVE_REWARD', group: null, + rewardTokenAddress: this.beetsAddress, + rewardTokenSymbol: 'BEETS', }); } @@ -140,6 +142,8 @@ export class MasterchefFarmAprService implements PoolAprService { apr: rewardApr, type: 'THIRD_PARTY_REWARD', group: null, + rewardTokenAddress: rewardToken.token, + rewardTokenSymbol: rewardToken.symbol, }); } } diff --git a/modules/pool/lib/apr-data-sources/fantom/reliquary-farm-apr.service.ts b/modules/pool/lib/apr-data-sources/fantom/reliquary-farm-apr.service.ts index 4634d63c5..9fee54476 100644 --- a/modules/pool/lib/apr-data-sources/fantom/reliquary-farm-apr.service.ts +++ b/modules/pool/lib/apr-data-sources/fantom/reliquary-farm-apr.service.ts @@ -128,6 +128,8 @@ export class ReliquaryFarmAprService implements PoolAprService { }, type: PrismaPoolAprType.NATIVE_REWARD, group: null, + rewardTokenAddress: this.beetsAddress, + rewardTokenSymbol: 'BEETS', }, }), ); diff --git a/modules/pool/lib/apr-data-sources/nested-pool-apr.service.ts b/modules/pool/lib/apr-data-sources/nested-pool-apr.service.ts index 43f83d365..e44ee5878 100644 --- a/modules/pool/lib/apr-data-sources/nested-pool-apr.service.ts +++ b/modules/pool/lib/apr-data-sources/nested-pool-apr.service.ts @@ -89,8 +89,15 @@ export class BoostedPoolAprService implements PoolAprService { apr: userApr, title: title, group: aprItem.group, + rewardTokenAddress: aprItem.rewardTokenAddress, + rewardTokenSymbol: aprItem.rewardTokenSymbol, + }, + update: { + apr: userApr, + title: title, + rewardTokenAddress: aprItem.rewardTokenAddress, + rewardTokenSymbol: aprItem.rewardTokenSymbol, }, - update: { apr: userApr, title: title }, }); } } diff --git a/modules/pool/lib/apr-data-sources/ve-bal-gauge-apr.service.ts b/modules/pool/lib/apr-data-sources/ve-bal-gauge-apr.service.ts index 1b00d5824..e24384636 100644 --- a/modules/pool/lib/apr-data-sources/ve-bal-gauge-apr.service.ts +++ b/modules/pool/lib/apr-data-sources/ve-bal-gauge-apr.service.ts @@ -58,7 +58,7 @@ export class GaugeAprService implements PoolAprService { // Get token rewards per year with data needed for the DB const rewards = await Promise.allSettled( - gauge.rewards.map(async ({ id, tokenAddress, rewardPerSecond }) => { + gauge.rewards.map(async ({ id, tokenAddress, rewardPerSecond, isVeBalemissions }) => { const price = this.tokenService.getPriceForToken(tokenPrices, tokenAddress, networkContext.chain); if (!price) { return Promise.reject(`Price not found for ${tokenAddress}`); @@ -79,6 +79,7 @@ export class GaugeAprService implements PoolAprService { address: tokenAddress, symbol: definition.symbol, rewardPerYear: parseFloat(rewardPerSecond) * secondsPerYear * price, + isVeBalemissions: isVeBalemissions, }; }), ); @@ -99,7 +100,7 @@ export class GaugeAprService implements PoolAprService { return null; } - const { address, symbol, rewardPerYear } = reward.value; + const { address, symbol, rewardPerYear, isVeBalemissions } = reward.value; const itemData: PrismaPoolAprItem = { id: `${reward.value.id}-${symbol}-apr`, @@ -108,16 +109,14 @@ export class GaugeAprService implements PoolAprService { title: `${symbol} reward APR`, group: null, apr: 0, - type: this.primaryTokens.includes(address.toLowerCase()) - ? PrismaPoolAprType.NATIVE_REWARD - : PrismaPoolAprType.THIRD_PARTY_REWARD, + rewardTokenAddress: address, + rewardTokenSymbol: symbol, + type: isVeBalemissions ? PrismaPoolAprType.NATIVE_REWARD : PrismaPoolAprType.THIRD_PARTY_REWARD, }; // veBAL rewards have a range associated with the item - if ( - itemData.id.includes('balgauge') && - (networkContext.chain === 'MAINNET' || gauge.version === 2) - ) { + // this is deprecated + if (isVeBalemissions && (networkContext.chain === 'MAINNET' || gauge.version === 2)) { let minApr = 0; if (workingSupplyTvl > 0) { minApr = rewardPerYear / workingSupplyTvl; diff --git a/modules/pool/lib/apr-data-sources/yb-tokens-apr.service.ts b/modules/pool/lib/apr-data-sources/yb-tokens-apr.service.ts index 9b22cd365..eef136ad5 100644 --- a/modules/pool/lib/apr-data-sources/yb-tokens-apr.service.ts +++ b/modules/pool/lib/apr-data-sources/yb-tokens-apr.service.ts @@ -93,6 +93,8 @@ export class YbTokensAprService implements PoolAprService { apr: userApr, group: tokenApr.group as PrismaPoolAprItemGroup, type: yieldType, + rewardTokenAddress: token.address, + rewardTokenSymbol: token.token.symbol, }; operations.push( diff --git a/modules/pool/lib/pool-gql-loader.service.ts b/modules/pool/lib/pool-gql-loader.service.ts index 260f0ccb3..aa439e269 100644 --- a/modules/pool/lib/pool-gql-loader.service.ts +++ b/modules/pool/lib/pool-gql-loader.service.ts @@ -35,6 +35,7 @@ import { GqlUserStakedBalance, GqlPoolFilterCategory, HookData, + GqlPoolAggregator, } from '../../../schema'; import { isSameAddress } from '@balancer-labs/sdk'; import _, { has, map } from 'lodash'; @@ -179,6 +180,20 @@ export class PoolGqlLoaderService { } } + public async getAggregatorPools(args: QueryPoolGetPoolsArgs): Promise { + // add limits per default + args.first = args.first || 1000; + args.skip = args.skip || 0; + + const pools = await prisma.prismaPool.findMany({ + ...this.mapQueryArgsToPoolQuery(args), + include: { + ...this.getPoolInclude(), + }, + }); + return pools.map((pool) => this.mapPoolToAggregatorPool(pool)); + } + public async getPools(args: QueryPoolGetPoolsArgs): Promise { // only include wallet and staked balances if the query requests it // this makes sure that we don't load ALL user balances when we don't filter on userAddress @@ -372,7 +387,7 @@ export class PoolGqlLoaderService { } const where = args.where; - const textSearch = args.textSearch ? { contains: args.textSearch.toLowerCase() } : undefined; + const textSearch = args.textSearch ? { contains: args.textSearch, mode: 'insensitive' as const } : undefined; const allTokensFilter = []; where?.tokensIn?.forEach((token) => { @@ -536,6 +551,69 @@ export class PoolGqlLoaderService { }; } + private mapPoolToAggregatorPool(pool: PrismaPoolWithExpandedNesting): GqlPoolAggregator { + const { typeData, ...poolWithoutTypeData } = pool; + + const mappedData = { + decimals: 18, + dynamicData: this.getPoolDynamicData(pool), + poolTokens: pool.tokens.map((token) => this.mapPoolToken(token, token.nestedPool !== null)), + vaultVersion: poolWithoutTypeData.protocolVersion, + }; + + switch (pool.type) { + case 'STABLE': + return { + ...poolWithoutTypeData, + ...(typeData as StableData), + ...mappedData, + }; + case 'META_STABLE': + return { + ...poolWithoutTypeData, + ...(typeData as StableData), + ...mappedData, + }; + case 'COMPOSABLE_STABLE': + return { + ...poolWithoutTypeData, + ...(typeData as StableData), + ...mappedData, + // bptPriceRate: bpt?.dynamicData?.priceRate || '1.0', + }; + case 'ELEMENT': + return { + ...poolWithoutTypeData, + ...(typeData as ElementData), + ...mappedData, + }; + case 'LIQUIDITY_BOOTSTRAPPING': + return { + ...poolWithoutTypeData, + ...mappedData, + }; + case 'GYRO': + case 'GYRO3': + case 'GYROE': + return { + ...poolWithoutTypeData, + ...(typeData as GyroData), + ...mappedData, + }; + case 'FX': + return { + ...poolWithoutTypeData, + ...mappedData, + ...(typeData as FxData), + }; + } + + return { + ...poolWithoutTypeData, + ...mappedData, + }; + } + private mapPoolToGqlPool( pool: PrismaPoolWithExpandedNesting, userWalletbalances: PrismaUserWalletBalance[] = [], @@ -1098,6 +1176,12 @@ export class PoolGqlLoaderService { let type: GqlPoolAprItemType; switch (aprItem.type) { case PrismaPoolAprType.NATIVE_REWARD: + if (pool.chain === 'FANTOM') { + type = 'MABEETS_EMISSIONS'; + } else { + type = 'VEBAL_EMISSIONS'; + } + break; case PrismaPoolAprType.THIRD_PARTY_REWARD: type = 'STAKING'; break; @@ -1115,12 +1199,16 @@ export class PoolGqlLoaderService { title: aprItem.title, apr: aprItem.range.min, type: type, + rewardTokenAddress: aprItem.rewardTokenAddress, + rewardTokenSymbol: aprItem.rewardTokenSymbol, }); aprItems.push({ id: `${aprItem.id}-boost`, title: aprItem.title, apr: aprItem.range.max - aprItem.range.min, type: 'STAKING_BOOST', + rewardTokenAddress: aprItem.rewardTokenAddress, + rewardTokenSymbol: aprItem.rewardTokenSymbol, }); } else { aprItems.push({ @@ -1128,6 +1216,8 @@ export class PoolGqlLoaderService { title: aprItem.title, apr: aprItem.apr, type: type, + rewardTokenAddress: aprItem.rewardTokenAddress, + rewardTokenSymbol: aprItem.rewardTokenSymbol, }); } } diff --git a/modules/pool/pool-debug.test.ts b/modules/pool/pool-debug.test.ts index 9e33fa128..65ba1498b 100644 --- a/modules/pool/pool-debug.test.ts +++ b/modules/pool/pool-debug.test.ts @@ -28,12 +28,15 @@ describe('pool debugging', () => { // // await CowAmmController().reloadPools('MAINNET'); // // await CowAmmController().syncSwaps('1'); // // await tokenService.updateTokenPrices(['MAINNET']); - await poolService.reloadAllPoolAprs('MAINNET'); + // await poolService.syncStakingForPools(['MAINNET']); // await poolService.updatePoolAprs('MAINNET'); const aprs = await prisma.prismaPoolAprItem.findMany({ where: { chain: 'MAINNET', poolId: '0xf08d4dea369c456d26a3168ff0024b904f2d8b91' }, }); console.log(aprs); + const pool = await poolService.getGqlPool('0xf08d4dea369c456d26a3168ff0024b904f2d8b91', 'MAINNET'); + expect(pool.dynamicData.aprItems).toBeDefined(); + expect(pool.dynamicData.aprItems.length).toBeGreaterThan(0); // await poolService.updatePoolAprs('MAINNET'); // expect(aprs[0].apr).toBeGreaterThan(0); diff --git a/modules/pool/pool.gql b/modules/pool/pool.gql index adb65cc10..77cc830a5 100644 --- a/modules/pool/pool.gql +++ b/modules/pool/pool.gql @@ -14,6 +14,17 @@ extend type Query { where: GqlPoolFilter textSearch: String ): [GqlPoolMinimal!]! + + """ + Returns all pools for a given filter, specific for aggregators + """ + poolGetAggregatorPools( + first: Int + skip: Int + orderBy: GqlPoolOrderBy + orderDirection: GqlPoolOrderDirection + where: GqlPoolFilter + ): [GqlPoolAggregator!]! """ Returns the number of pools for a given filter. """ @@ -285,6 +296,145 @@ enum GqlPoolType { COW_AMM } +type GqlPoolAggregator { + """ + The pool id. This is equal to the address for protocolVersion 3 pools + """ + id: ID! + """ + The chain on which the pool is deployed + """ + chain: GqlChain! + """ + The protocol version on which the pool is deployed, 1, 2 or 3 + """ + protocolVersion: Int! + """ + The pool type, such as weighted, stable, etc. + """ + type: GqlPoolType! + """ + The name of the pool as per contract + """ + name: String! + """ + The token symbol of the pool as per contract + """ + symbol: String! + """ + The contract address of the pool. + """ + address: Bytes! + """ + The decimals of the BPT, usually 18 + """ + decimals: Int! + """ + The wallet address of the owner of the pool. Pool owners can set certain properties like swapFees or AMP. + """ + owner: Bytes + """ + The factory contract address from which the pool was created. + """ + factory: Bytes + """ + The timestamp the pool was created. + """ + createTime: Int! + """ + The version of the pool type. + """ + version: Int! + """ + Returns all pool tokens, including BPTs and nested pools if there are any. Only one nested level deep. + """ + poolTokens: [GqlPoolTokenDetail!]! + """ + Dynamic data such as token balances, swap fees or volume + """ + dynamicData: GqlPoolDynamicData! + """ + Data specific to gyro/fx pools + """ + alpha: String + """ + Data specific to gyro/fx pools + """ + beta: String + """ + Data specific to gyro pools + """ + sqrtAlpha: String + """ + Data specific to gyro pools + """ + sqrtBeta: String + """ + Data specific to gyro pools + """ + root3Alpha: String + """ + Data specific to gyro pools + """ + c: String + """ + Data specific to gyro pools + """ + s: String + """ + Data specific to gyro/fx pools + """ + lambda: String + """ + Data specific to gyro pools + """ + tauAlphaX: String + """ + Data specific to gyro pools + """ + tauAlphaY: String + """ + Data specific to gyro pools + """ + tauBetaX: String + """ + Data specific to gyro pools + """ + tauBetaY: String + """ + Data specific to gyro pools + """ + u: String + """ + Data specific to gyro pools + """ + v: String + """ + Data specific to gyro pools + """ + w: String + """ + Data specific to gyro pools + """ + z: String + """ + Data specific to gyro pools + """ + dSq: String + """ + Data specific to fx pools + """ + delta: String + """ + Data specific to fx pools + """ + epsilon: String + """ + Data specific to stable pools + """ + amp: BigInt +} + """ The base type as returned by poolGetPool (specific pool query) """ @@ -1207,12 +1357,20 @@ type GqlPoolAprItem { """ The title of the APR item, a human readable form """ - title: String! + title: String! @deprecated(reason: "No replacement, should be built client side") """ The APR value in % -> 0.2 = 0.2% """ apr: Float! """ + The reward token address, if the APR originates from token emissions + """ + rewardTokenAddress: String + """ + The reward token symbol, if the APR originates from token emissions + """ + rewardTokenSymbol: String + """ Specific type of this APR """ type: GqlPoolAprItemType! @@ -1226,47 +1384,46 @@ enum GqlPoolAprItemType { Represents the swap fee APR in a pool. """ SWAP_FEE - """ Represents the yield from an IB (Interest-Bearing) asset APR in a pool. """ IB_YIELD - """ Represents if the APR items comes from a nested pool. """ NESTED - """ - Staking reward APR in a pool, i.e. BAL or BEETS. + Staking reward APR in a pool from a reward token. """ STAKING - + """ + Reward APR in a pool from veBAL emissions allocated by gauge votes. Emitted in BAL. + """ + VEBAL_EMISSIONS + """ + Reward APR in a pool from maBEETS emissions allocated by gauge votes. Emitted in BEETS. + """ + MABEETS_EMISSIONS """ APR boost that can be earned, i.e. via veBAL or maBEETS. """ STAKING_BOOST - """ APR in a pool that can be earned through locking, i.e. veBAL """ LOCKING - """ APR that can be earned thourgh voting, i.e. gauge votes """ VOTING - """ APR that pools earns when BPT is staked on AURA. """ AURA - """ Rewards distributed by merkl.xyz """ MERKL - """ Cow AMM specific APR """ diff --git a/modules/pool/pool.resolvers.ts b/modules/pool/pool.resolvers.ts index 03ae1a221..5d6f947b4 100644 --- a/modules/pool/pool.resolvers.ts +++ b/modules/pool/pool.resolvers.ts @@ -22,6 +22,9 @@ const balancerResolvers: Resolvers = { poolGetPools: async (parent, args, context) => { return poolService.getGqlPools(args); }, + poolGetAggregatorPools: async (parent, args, context) => { + return poolService.getAggregatorPools(args); + }, poolGetPoolsCount: async (parent, args, context) => { return poolService.getPoolsCount(args); }, diff --git a/modules/pool/pool.service.ts b/modules/pool/pool.service.ts index dab3ee197..62848cc86 100644 --- a/modules/pool/pool.service.ts +++ b/modules/pool/pool.service.ts @@ -4,6 +4,7 @@ import moment from 'moment-timezone'; import { prisma } from '../../prisma/prisma-client'; import { GqlChain, + GqlPoolAggregator, GqlPoolBatchSwap, GqlPoolFeaturedPool, GqlPoolFeaturedPoolGroup, @@ -84,6 +85,10 @@ export class PoolService { return this.poolGqlLoaderService.getPools(args); } + public async getAggregatorPools(args: QueryPoolGetPoolsArgs): Promise { + return this.poolGqlLoaderService.getAggregatorPools(args); + } + public async getPoolsCount(args: QueryPoolGetPoolsArgs): Promise { return this.poolGqlLoaderService.getPoolsCount(args); } diff --git a/modules/sources/viem-client.ts b/modules/sources/viem-client.ts index 625856753..e29468130 100644 --- a/modules/sources/viem-client.ts +++ b/modules/sources/viem-client.ts @@ -1,4 +1,4 @@ -import { createPublicClient, http } from 'viem'; +import { createPublicClient, http, PublicClient } from 'viem'; import { arbitrum, avalanche, @@ -18,6 +18,12 @@ import config from '../../config'; export type ViemClient = ReturnType; +// Use this interface for easier mocking +export interface IViemClient { + multicall: PublicClient['multicall']; + readContract: PublicClient['readContract']; +} + const chain2ViemChain = { [Chain.MAINNET]: mainnet, [Chain.SEPOLIA]: sepolia, diff --git a/modules/subgraphs/reliquary-subgraph/generated/reliquary-subgraph-types.ts b/modules/subgraphs/reliquary-subgraph/generated/reliquary-subgraph-types.ts index 45200c463..f27b6a95a 100644 --- a/modules/subgraphs/reliquary-subgraph/generated/reliquary-subgraph-types.ts +++ b/modules/subgraphs/reliquary-subgraph/generated/reliquary-subgraph-types.ts @@ -17,14 +17,8 @@ export type Scalars = { BigInt: string; Bytes: string; Int8: any; - Timestamp: any; }; -export enum Aggregation_Interval { - day = 'day', - hour = 'hour', -} - export type BlockChangedFilter = { number_gte: Scalars['Int']; }; @@ -2036,8 +2030,6 @@ export type _Block_ = { hash?: Maybe; /** The block number */ number: Scalars['Int']; - /** The hash of the parent block */ - parentHash?: Maybe; /** Integer representation of the timestamp stored in blocks for the chain */ timestamp?: Maybe; }; diff --git a/modules/vebal/__snapshots__/voting-gauges.repository.spec.ts.snap b/modules/vebal/__snapshots__/voting-gauges.repository.spec.ts.snap new file mode 100644 index 000000000..4e342cb73 --- /dev/null +++ b/modules/vebal/__snapshots__/voting-gauges.repository.spec.ts.snap @@ -0,0 +1,43 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`VotingGaugesRepository > fetches veBAL gauge as MAINNET 1`] = ` +[ + { + "gaugeAddress": "0xe867ad0a48e8f815dc0cda2cdb275e0f163a480b", + "isInSubgraph": false, + "isKilled": "0", + "network": "MAINNET", + "relativeWeight": NaN, + "relativeWeightCap": undefined, + }, +] +`; + +exports[`VotingGaugesRepository > generates voting gauge rows given a list of gauge addresses 1`] = ` +[ + { + "gaugeAddress": "0x79ef6103a513951a3b25743db509e267685726b7", + "isInSubgraph": false, + "isKilled": "0.07308250729037725", + "network": "MAINNET", + "relativeWeight": NaN, + "relativeWeightCap": undefined, + }, + { + "gaugeAddress": "0xfb0265841c49a6b19d70055e596b212b0da3f606", + "isInSubgraph": false, + "isKilled": "0", + "network": "OPTIMISM", + "relativeWeight": NaN, + "relativeWeightCap": undefined, + }, + { + "gaugeAddress": "0x8f7a0f9cf545db78bf5120d3dbea7de9c6220c10", + "isInSubgraph": false, + "isKilled": "0", + "network": "ARBITRUM", + "relativeWeight": NaN, + "relativeWeightCap": "0.000000000000000.02", + }, +] +`; diff --git a/modules/vebal/prismaPoolStakingGauge.mock.ts b/modules/vebal/prismaPoolStakingGauge.mock.ts index 9d472c4ee..17ab546c2 100644 --- a/modules/vebal/prismaPoolStakingGauge.mock.ts +++ b/modules/vebal/prismaPoolStakingGauge.mock.ts @@ -1,5 +1,6 @@ +import { vi } from 'vitest'; import { mockDeep } from 'vitest-mock-extended'; -import { prisma as prismaClient } from '../../prisma/prisma-client'; +import { PrismaClient } from '@prisma/client'; import { PrismaPoolStakingGauge } from '.prisma/client'; import { Chain } from '@prisma/client'; @@ -17,7 +18,12 @@ export function aPrismaPoolStakingGauge(...options: Partial(); +export const prismaMock = mockDeep({ + prismaPoolStakingGauge: { + findFirst: vi.fn(), + // Add other methods you use + }, +}); export const defaultStakingGaugeId = '0x79ef6103a513951a3b25743db509e267685726b7'; export const defaultStakingGauge = aPrismaPoolStakingGauge({ id: defaultStakingGaugeId }); diff --git a/modules/vebal/voting-gauges.repository.spec.ts b/modules/vebal/voting-gauges.repository.spec.ts index 7f35f11d5..2c4b27139 100644 --- a/modules/vebal/voting-gauges.repository.spec.ts +++ b/modules/vebal/voting-gauges.repository.spec.ts @@ -1,92 +1,81 @@ -import { setMainnetRpcProviderForTesting } from '../../test/utils'; -import { defaultStakingGaugeId, prismaMock } from './prismaPoolStakingGauge.mock'; -import { VotingGauge, VotingGaugesRepository } from './voting-gauges.repository'; import { Chain } from '@prisma/client'; +import { VotingGauge, VotingGaugesRepository } from './voting-gauges.repository'; +import { defaultStakingGaugeId, prismaMock } from './prismaPoolStakingGauge.mock'; +import { MockViemClient } from '../../test/mock-viem-client'; -const httpRpc = 'http://127.0.0.1:8555'; -setMainnetRpcProviderForTesting(httpRpc); +// Create a mock viemClient +const mockViemClient = new MockViemClient(); -it('maps onchain network format into prisma chain format', async () => { - const repository = new VotingGaugesRepository(); - expect(repository.toPrismaNetwork('Mainnet')).toBe(Chain.MAINNET); - expect(repository.toPrismaNetwork('Optimism')).toBe(Chain.OPTIMISM); - expect(repository.toPrismaNetwork('veBAL')).toBe(Chain.MAINNET); - expect(repository.toPrismaNetwork('POLYGONZKEVM')).toBe(Chain.ZKEVM); - expect(() => repository.toPrismaNetwork('Unknown')).toThrowError('Network UNKNOWN is not supported'); -}); +// Create a repository instance with mocked dependencies +const repository = new VotingGaugesRepository(prismaMock, mockViemClient as any); -it('fetches list of voting gauge addresses', async () => { - const repository = new VotingGaugesRepository(); - const addresses = await repository.getVotingGaugeAddresses(); - expect(addresses.length).toBe(373); -}, 10_000); - -it('generates voting gauge rows given a list of gauge addresses', async () => { - const repository = new VotingGaugesRepository(); - - const votingGaugeAddresses = [ - '0x79eF6103A513951a3b25743DB509E267685726B7', - '0xfb0265841C49A6b19D70055E596b212B0dA3f606', - '0x8F7a0F9cf545DB78BF5120D3DBea7DE9c6220c10', - ]; - // Uncomment to test with all the root gauges - // const votingGaugeAddresses = await service.getVotingGaugeAddresses(); - - const rows = await repository.fetchOnchainVotingGauges(votingGaugeAddresses); - - expect(rows).toMatchInlineSnapshot(` - [ - { - "gaugeAddress": "0x79ef6103a513951a3b25743db509e267685726b7", - "isInSubgraph": false, - "isKilled": false, - "network": "MAINNET", - "relativeWeight": 0.07308250729037725, - "relativeWeightCap": undefined, - }, - { - "gaugeAddress": "0xfb0265841c49a6b19d70055e596b212b0da3f606", - "isInSubgraph": false, - "isKilled": true, - "network": "OPTIMISM", - "relativeWeight": 0, - "relativeWeightCap": undefined, - }, - { - "gaugeAddress": "0x8f7a0f9cf545db78bf5120d3dbea7de9c6220c10", - "isInSubgraph": false, - "isKilled": false, - "network": "ARBITRUM", - "relativeWeight": 0, - "relativeWeightCap": "0.02", - }, - ] - `); -}, 10_000); - -it('Excludes Liquidity Mining Committee gauge', async () => { - const liquidityMiningAddress = '0x7AA5475b2eA29a9F4a1B9Cf1cB72512D1B4Ab75e'; - const repository = new VotingGaugesRepository(); - const rows = await repository.fetchOnchainVotingGauges([liquidityMiningAddress]); - expect(rows).toEqual([]); -}); +describe('VotingGaugesRepository', () => { + it('maps onchain network format into prisma chain format', () => { + expect(repository.toPrismaNetwork('Mainnet')).toBe(Chain.MAINNET); + expect(repository.toPrismaNetwork('Optimism')).toBe(Chain.OPTIMISM); + expect(repository.toPrismaNetwork('veBAL')).toBe(Chain.MAINNET); + expect(repository.toPrismaNetwork('POLYGONZKEVM')).toBe(Chain.ZKEVM); + expect(() => repository.toPrismaNetwork('Unknown')).toThrowError('Network UNKNOWN is not supported'); + }); + + it('fetches list of voting gauge addresses', async () => { + const mockAddresses = Array(373).fill('0x1234567890123456789012345678901234567890'); + mockViemClient.mockMulticallResult(mockAddresses); + mockViemClient.mockReadContractResult('n_gauges', 373); + + const addresses = await repository.getVotingGaugeAddresses(); + expect(addresses).toHaveLength(373); + }); + + it('generates voting gauge rows given a list of gauge addresses', async () => { + const votingGaugeAddresses = [ + '0x79eF6103A513951a3b25743DB509E267685726B7', + '0xfb0265841C49A6b19D70055E596b212B0dA3f606', + '0x8F7a0F9cf545DB78BF5120D3DBea7DE9c6220c10', + ]; + + mockViemClient.mockMulticallResult([false, true, false]); // isKilled + mockViemClient.mockMulticallResult(['0.07308250729037725', '0', '0']); // relativeWeights + mockViemClient.mockMulticallResult([undefined, undefined, '0.02']); // relativeWeightCaps + mockViemClient.mockMulticallResult(['Ethereum', 'Optimism', 'Arbitrum']); // gaugeType names + mockViemClient.mockMulticallResult([0, 1, 2]); // gaugeTypes + mockViemClient.mockReadContractResult('n_gauge_types', 3); + + const rows = await repository.fetchOnchainVotingGauges(votingGaugeAddresses); + + expect(rows).toMatchSnapshot(); + }); + + it('Excludes Liquidity Mining Committee gauge', async () => { + const liquidityMiningAddress = '0x7AA5475b2eA29a9F4a1B9Cf1cB72512D1B4Ab75e'; + + mockViemClient.mockMulticallResult([false]); // isKilled + mockViemClient.mockMulticallResult(['0.01']); // relativeWeights + mockViemClient.mockMulticallResult(['0.02']); // relativeWeightCaps + mockViemClient.mockMulticallResult(['Liquidity Mining Committee']); // gaugeType names + mockViemClient.mockMulticallResult([0]); // gaugeTypes + mockViemClient.mockReadContractResult('n_gauge_types', 1); + + const rows = await repository.fetchOnchainVotingGauges([liquidityMiningAddress]); + expect(rows).toEqual([]); + }); + + it('fetches veBAL gauge as MAINNET', async () => { + const vebalAddress = '0xE867AD0a48e8f815DC0cda2CDb275e0F163A480b'; + + mockViemClient.mockMulticallResult([true]); // isKilled + mockViemClient.mockMulticallResult(['0']); // relativeWeights + mockViemClient.mockMulticallResult([undefined]); // relativeWeightCaps + mockViemClient.mockMulticallResult(['veBAL']); // gaugeTypes + mockViemClient.mockMulticallResult([0]); // gaugeTypes + mockViemClient.mockReadContractResult('n_gauge_types', 1); -it('fetches veBAL gauge as MAINNET', async () => { - const vebalAddress = '0xE867AD0a48e8f815DC0cda2CDb275e0F163A480b'; - const repository = new VotingGaugesRepository(); - const rows = await repository.fetchOnchainVotingGauges([vebalAddress]); - expect(rows).toEqual([ - { - gaugeAddress: '0xe867ad0a48e8f815dc0cda2cdb275e0f163a480b', - isInSubgraph: false, - isKilled: true, - network: 'MAINNET', - relativeWeight: 0, - relativeWeightCap: undefined, - }, - ]); + const rows = await repository.fetchOnchainVotingGauges([vebalAddress]); + expect(rows).toMatchSnapshot(); + }); }); +// Helper function to create a VotingGauge object export function aVotingGauge(...options: Partial[]): VotingGauge { const defaultRootGauge: VotingGauge = { gaugeAddress: '0x79ef6103a513951a3b25743db509e267685726b7', @@ -100,37 +89,39 @@ export function aVotingGauge(...options: Partial[]): VotingGauge { return Object.assign({}, defaultRootGauge, ...options); } -const EmptyError = new Error(); - -const repository = new VotingGaugesRepository(prismaMock); +describe('VotingGaugesRepository saving gauges', () => { + it('successfully saves onchain gauges', async () => { + const votingGauge = aVotingGauge({ network: Chain.OPTIMISM }); -it('successfully saves onchain gauges', async () => { - const votingGauge = aVotingGauge({ network: Chain.OPTIMISM }); + const { votingGaugesWithStakingGaugeId: votingGauges, saveErrors } = await repository.saveVotingGauges([ + votingGauge, + ]); - const { votingGaugesWithStakingGaugeId: votingGauges } = await repository.saveVotingGauges([votingGauge]); - - expect(votingGauges[0]).toMatchObject(votingGauge); - expect(votingGauges[0].stakingGaugeId).toBe(defaultStakingGaugeId); -}); - -describe('When staking gauge is not found ', () => { - beforeEach(() => prismaMock.prismaPoolStakingGauge.findFirst.mockResolvedValue(null)); + expect(votingGauges[0]).toMatchObject(votingGauge); + expect(votingGauges[0].stakingGaugeId).toBe(defaultStakingGaugeId); + expect(saveErrors).toHaveLength(0); + }); - it('has errors when gauge is valid for voting (not killed)', async () => { - const repository = new VotingGaugesRepository(prismaMock); + describe('When staking gauge is not found ', () => { + beforeEach(() => prismaMock.prismaPoolStakingGauge.findFirst.mockResolvedValue(null)); - const votingGauge = aVotingGauge({ network: Chain.MAINNET, isKilled: false }); + it('has errors when gauge is valid for voting (not killed)', async () => { + const votingGauge = aVotingGauge({ network: Chain.MAINNET, isKilled: false }); - const { votingGaugesWithStakingGaugeId, saveErrors } = await repository.saveVotingGauges([votingGauge]); + const { saveErrors } = await repository.saveVotingGauges([votingGauge]); - expect(saveErrors.length).toBe(1); - }); + expect(saveErrors).toHaveLength(1); + expect(saveErrors[0].message).toContain('Failed to save voting gauge'); + }); - it('does not throw when gauge is valid for voting (killed with no votes)', async () => { - const votingGauge = aVotingGauge({ network: Chain.MAINNET, isKilled: true, relativeWeight: 0 }); + it('does not throw when gauge is invalid for voting (killed with no votes)', async () => { + const votingGauge = aVotingGauge({ network: Chain.MAINNET, isKilled: true, relativeWeight: 0 }); - const result = await repository.saveVotingGauges([votingGauge]); + const { votingGaugesWithStakingGaugeId, saveErrors } = await repository.saveVotingGauges([votingGauge]); - expect(result).toBeDefined(); + expect(votingGaugesWithStakingGaugeId).toHaveLength(1); + expect(votingGaugesWithStakingGaugeId[0].stakingGaugeId).toBeUndefined(); + expect(saveErrors).toHaveLength(0); + }); }); }); diff --git a/modules/vebal/voting-gauges.repository.ts b/modules/vebal/voting-gauges.repository.ts index df7168bb8..752ffc909 100644 --- a/modules/vebal/voting-gauges.repository.ts +++ b/modules/vebal/voting-gauges.repository.ts @@ -1,22 +1,17 @@ import { Chain } from '@prisma/client'; -import { keyBy, mapValues } from 'lodash'; +import { keyBy } from 'lodash'; -import { formatFixed } from '@ethersproject/bignumber'; -import { BigNumber, Contract } from 'ethers'; -import { formatEther } from 'ethers/lib/utils'; -import { mainnetNetworkConfig } from '../network/mainnet'; +import mainnet from '../../config/mainnet'; import gaugeControllerAbi from './abi/gaugeController.json'; -import gaugeControllerHelperAbi from './abi/gaugeControllerHelper.json'; import rootGaugeAbi from './abi/rootGauge.json'; import { PrismaClient } from '@prisma/client'; import { prisma as prismaClient } from '../../prisma/prisma-client'; import { v1RootGaugeRecipients } from './special-pools/streamer-v1-gauges'; -import { Multicaller3 } from '../web3/multicaller3'; import { GaugeSubgraphService } from '../subgraphs/gauge-subgraph/gauge-subgraph.service'; +import { formatEther } from 'viem'; +import { getViemClient, IViemClient } from '../sources/viem-client'; -const gaugeControllerAddress = mainnetNetworkConfig.data.gaugeControllerAddress!; -// Helper contract that wraps gaugeControllerAddress contract to allow checkpointing and getting the updated relative weight -const gaugeControllerHelperAddress = mainnetNetworkConfig.data.gaugeControllerHelperAddress!; +const { gaugeControllerAddress, gaugeControllerHelperAddress } = mainnet; export type VotingGauge = { gaugeAddress: string; @@ -42,32 +37,138 @@ type SubGraphGauge = { * Saves voting gauges in prisma DB */ export class VotingGaugesRepository { - constructor(private prisma: PrismaClient = prismaClient) {} + constructor( + private prisma: PrismaClient = prismaClient, + private viemClient: IViemClient = getViemClient('MAINNET'), + ) {} + + async getVotingGaugeAddresses() { + const totalGauges = await this.viemClient + .readContract({ + address: gaugeControllerAddress as `0x${string}`, + functionName: 'n_gauges', + abi: gaugeControllerAbi, + }) + .then(Number); + + const contracts = Array.from({ length: totalGauges }, (_, index) => ({ + abi: gaugeControllerAbi as any, + address: gaugeControllerAddress as `0x${string}`, + functionName: 'gauges', + args: [index], + })); + + const addresses = await this.viemClient + .multicall({ contracts, allowFailure: false }) + .then((results) => results.map((address) => (address as string).toLowerCase())); + + return addresses; + } - async getVotingGaugeAddresses(): Promise { - const totalGauges = Number(formatFixed(await this.getGaugeControllerContract().n_gauges())); - return await this.fetchGaugeAddresses(totalGauges); + async fetchTypeNames() { + const totalGaugesTypes = await this.viemClient + .readContract({ + address: gaugeControllerAddress as `0x${string}`, + functionName: 'n_gauge_types', + abi: gaugeControllerAbi, + }) + .then(Number); + + const contracts = Array.from({ length: totalGaugesTypes }, (_, index) => ({ + abi: gaugeControllerAbi as any, + address: gaugeControllerAddress as `0x${string}`, + functionName: 'gauge_type_names', + args: [index], + })); + + const typeNames = (await this.viemClient.multicall({ contracts, allowFailure: false })) as string[]; + + return typeNames; } - async fetchOnchainVotingGauges(gaugeAddresses: string[]): Promise { - const totalGaugesTypes = Number(formatFixed(await this.getGaugeControllerContract().n_gauge_types())); + // Many of the root contracts do not have getRelativeWeightCap function defined, so expect undefined values + async fetchRelativeWeightCaps(gaugeAddresses: string[]): Promise> { + const contracts = gaugeAddresses.map((address) => ({ + abi: rootGaugeAbi as any, + address: address as `0x${string}`, + functionName: 'getRelativeWeightCap', + })); + + const results = await this.viemClient.multicall({ contracts }); + const caps = gaugeAddresses.map((address, index) => { + const result = results[index]; + const cap = result.status === 'success' ? formatEther(result.result as bigint) : undefined; + return [address, cap]; + }); - const typeNames = await this.fetchTypeNames(totalGaugesTypes); + return Object.fromEntries(caps); + } - const relativeWeights = await this.fetchRelativeWeights(gaugeAddresses); + /* + gauge_types are not reliable because they are manually input by Maxis + We will use subgraph chain field instead + However, we keep pulling this gauge_types cause they can be useful for debugging (when a root gauge is not found in the subgraph) + */ + async fetchGaugeTypes(gaugeAddresses: string[]) { + const typeNames = await this.fetchTypeNames(); + + const contracts = gaugeAddresses.map((address) => ({ + abi: gaugeControllerAbi as any, + address: gaugeControllerAddress as `0x${string}`, + functionName: 'gauge_types', + args: [address], + })); + + const results = await this.viemClient.multicall({ contracts, allowFailure: false }); + const types = gaugeAddresses.map((address, index) => { + const type = results[index]; + return [address, typeNames[Number(type)]] as [string, string]; + }); - /* - gauge_types are not reliable because they are manually input by Maxis - We will use subgraph chain field instead - However, we keep pulling this gauge_types cause they can be useful for debugging (when a root gauge is not found in the subgraph) - */ - const gaugeTypeIndexes = await this.fetchGaugeTypes(gaugeAddresses); - const gaugeTypes = mapValues(gaugeTypeIndexes, (type) => typeNames[Number(type)]); + return Object.fromEntries(types); + } + + async fetchRelativeWeights(gaugeAddresses: string[]) { + const contracts = gaugeAddresses.map((address) => ({ + abi: gaugeControllerAbi as any, + address: gaugeControllerHelperAddress as `0x${string}`, + functionName: 'gauge_relative_weight', + args: [address], + })); + + const results = await this.viemClient.multicall({ contracts, allowFailure: false }); + + const weigths = gaugeAddresses.map((address, index) => { + let weight = results[index] as bigint; + return [address, Number(formatEther(weight))] as [string, number]; + }); + + return Object.fromEntries(weigths); + } + + async fetchIsKilled(gaugeAddresses: string[]) { + const contracts = gaugeAddresses.map((address) => ({ + abi: rootGaugeAbi as any, + address: address as `0x${string}`, + functionName: 'is_killed', + })); + const results = await this.viemClient.multicall({ contracts, allowFailure: false }); + const kills = gaugeAddresses.map((address, index) => { + return [address, results[index] as boolean]; + }); + + return Object.fromEntries(kills); + } + + async fetchOnchainVotingGauges(gaugeAddresses: string[]): Promise { + const relativeWeights = await this.fetchRelativeWeights(gaugeAddresses); const isKilled = await this.fetchIsKilled(gaugeAddresses); const relativeWeightCaps = await this.fetchRelativeWeightCaps(gaugeAddresses); + const gaugeTypes = await this.fetchGaugeTypes(gaugeAddresses); + let votingGauges: VotingGauge[] = []; gaugeAddresses.forEach((gaugeAddress) => { if (gaugeTypes[gaugeAddress] === 'Liquidity Mining Committee') return; @@ -76,9 +177,7 @@ export class VotingGaugesRepository { network: this.toPrismaNetwork(gaugeTypes[gaugeAddress]), isKilled: isKilled[gaugeAddress], relativeWeight: relativeWeights[gaugeAddress], - relativeWeightCap: relativeWeightCaps[gaugeAddress] - ? formatEther(relativeWeightCaps[gaugeAddress]!) - : undefined, + relativeWeightCap: relativeWeightCaps[gaugeAddress], isInSubgraph: false, }); }); @@ -88,7 +187,7 @@ export class VotingGaugesRepository { async fetchVotingGaugesFromSubgraph(onchainAddresses: string[]) { // This service only works with the mainnet subgraph, will return no voting gauges for other chains - const gaugeSubgraphService = new GaugeSubgraphService(mainnetNetworkConfig.data.subgraphs.gauge!); + const gaugeSubgraphService = new GaugeSubgraphService(mainnet.subgraphs.gauge!); const rootGauges = await gaugeSubgraphService.getRootGaugesForIds(onchainAddresses); const l2RootGauges: SubGraphGauge[] = rootGauges.map((gauge) => { @@ -114,10 +213,6 @@ export class VotingGaugesRepository { return [...l2RootGauges, ...mainnetLiquidityGauges]; } - async deleteVotingGauges() { - await this.prisma.prismaVotingGauge.deleteMany(); - } - async saveVotingGauges(votingGauges: VotingGauge[]) { const saveErrors: Error[] = []; const votingGaugesWithStakingGaugeId = await Promise.all( @@ -227,86 +322,6 @@ export class VotingGaugesRepository { }); } - /** - * We need to use multicall3 with allowFailures=true because many of the root contracts do not have getRelativeWeightCap function defined - */ - async fetchRelativeWeightCaps(gaugeAddresses: string[]) { - const multicall3 = new Multicaller3(rootGaugeAbi, 50); - - gaugeAddresses.forEach((address) => { - multicall3.call(address, address, 'getRelativeWeightCap'); - }); - - return (await multicall3.execute()) as Record; - } - - getGaugeControllerContract() { - return new Contract(gaugeControllerAddress, gaugeControllerAbi, mainnetNetworkConfig.provider); - } - - async fetchGaugeAddresses(totalGauges: number) { - const multicaller = this.buildGaugeControllerMulticaller(); - this.generateGaugeIndexes(totalGauges).forEach((index) => - multicaller.call(`${index}`, gaugeControllerAddress, 'gauges', [index]), - ); - - const response = (await multicaller.execute()) as Record; - return Object.values(response).map((address) => address.toLowerCase()); - } - - async fetchTypeNames(totalTypes: number) { - const multicaller = this.buildGaugeControllerMulticaller(); - - this.generateGaugeIndexes(totalTypes).forEach((index) => - multicaller.call(`${index}`, gaugeControllerAddress, 'gauge_type_names', [index]), - ); - - const response = (await multicaller.execute()) as Record; - - return Object.values(response); - } - - async fetchGaugeTypes(gaugeAddresses: string[]) { - const multicaller = this.buildGaugeControllerMulticaller(); - - gaugeAddresses.forEach((address) => - multicaller.call(address, gaugeControllerAddress, 'gauge_types', [address]), - ); - - return (await multicaller.execute()) as Record; - } - - async fetchRelativeWeights(gaugeAddresses: string[]) { - const multicaller = this.buildGaugeControllerHelperMulticaller(); - gaugeAddresses.forEach((address) => - multicaller.call(address, gaugeControllerHelperAddress, 'gauge_relative_weight', [address], false), - ); - - const response = (await multicaller.execute()) as Record; - - return mapValues(response, (value) => Number(formatEther(value))); - } - - async fetchIsKilled(gaugeAddresses: string[]) { - const rootGaugeMulticaller = new Multicaller3(rootGaugeAbi); - - gaugeAddresses.forEach((address) => rootGaugeMulticaller.call(address, address, 'is_killed')); - - return (await rootGaugeMulticaller.execute()) as Record; - } - - buildGaugeControllerMulticaller() { - return new Multicaller3(gaugeControllerAbi); - } - - buildGaugeControllerHelperMulticaller() { - return new Multicaller3(gaugeControllerHelperAbi); - } - - generateGaugeIndexes(totalGauges: number) { - return [...Array(totalGauges)].map((_, index) => index); - } - toPrismaNetwork(chainOrSubgraphNetwork: string): Chain { const network = chainOrSubgraphNetwork.toUpperCase(); if (network === 'ETHEREUM') return Chain.MAINNET; diff --git a/prisma/migrations/20240828145122_add_reward_token_info/migration.sql b/prisma/migrations/20240828145122_add_reward_token_info/migration.sql new file mode 100644 index 000000000..55c1dca8a --- /dev/null +++ b/prisma/migrations/20240828145122_add_reward_token_info/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "PrismaPoolAprItem" ADD COLUMN "rewardTokenAddress" TEXT, +ADD COLUMN "rewardTokenSymbol" TEXT; + +-- AlterTable +ALTER TABLE "PrismaPoolStakingGaugeReward" ADD COLUMN "isVeBalemissions" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 40701e409..a3e75199a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -296,6 +296,8 @@ model PrismaPoolAprItem { pool PrismaPool @relation(fields:[poolId, chain], references: [id, chain], onDelete: Cascade) chain Chain title String + rewardTokenAddress String? + rewardTokenSymbol String? apr Float range PrismaPoolAprRange? @@ -495,6 +497,7 @@ model PrismaPoolStakingGaugeReward{ tokenAddress String rewardPerSecond String + isVeBalemissions Boolean @default(false) } diff --git a/prisma/schema/pool.prisma b/prisma/schema/pool.prisma index 43c756971..10ca0b896 100644 --- a/prisma/schema/pool.prisma +++ b/prisma/schema/pool.prisma @@ -222,6 +222,8 @@ model PrismaPoolAprItem { pool PrismaPool @relation(fields:[poolId, chain], references: [id, chain], onDelete: Cascade) chain Chain title String + rewardTokenAddress String? + rewardTokenSymbol String? apr Float range PrismaPoolAprRange? @@ -421,6 +423,7 @@ model PrismaPoolStakingGaugeReward{ tokenAddress String rewardPerSecond String + isVeBalemissions Boolean @default(false) } diff --git a/schema.ts b/schema.ts index 965e094ea..e6c82fb3e 100644 --- a/schema.ts +++ b/schema.ts @@ -199,6 +199,78 @@ export interface GqlPoolAddRemoveEventV3 extends GqlPoolEvent { valueUSD: Scalars['Float']; } +export interface GqlPoolAggregator { + __typename?: 'GqlPoolAggregator'; + /** The contract address of the pool. */ + address: Scalars['Bytes']; + /** Data specific to gyro/fx pools */ + alpha?: Maybe; + /** Data specific to stable pools */ + amp?: Maybe; + /** Data specific to gyro/fx pools */ + beta?: Maybe; + /** Data specific to gyro pools */ + c?: Maybe; + /** The chain on which the pool is deployed */ + chain: GqlChain; + /** The timestamp the pool was created. */ + createTime: Scalars['Int']; + /** Data specific to gyro pools */ + dSq?: Maybe; + /** The decimals of the BPT, usually 18 */ + decimals: Scalars['Int']; + /** Data specific to fx pools */ + delta?: Maybe; + /** Dynamic data such as token balances, swap fees or volume */ + dynamicData: GqlPoolDynamicData; + /** Data specific to fx pools */ + epsilon?: Maybe; + /** The factory contract address from which the pool was created. */ + factory?: Maybe; + /** The pool id. This is equal to the address for protocolVersion 3 pools */ + id: Scalars['ID']; + /** Data specific to gyro/fx pools */ + lambda?: Maybe; + /** The name of the pool as per contract */ + name: Scalars['String']; + /** The wallet address of the owner of the pool. Pool owners can set certain properties like swapFees or AMP. */ + owner?: Maybe; + /** Returns all pool tokens, including BPTs and nested pools if there are any. Only one nested level deep. */ + poolTokens: Array; + /** The protocol version on which the pool is deployed, 1, 2 or 3 */ + protocolVersion: Scalars['Int']; + /** Data specific to gyro pools */ + root3Alpha?: Maybe; + /** Data specific to gyro pools */ + s?: Maybe; + /** Data specific to gyro pools */ + sqrtAlpha?: Maybe; + /** Data specific to gyro pools */ + sqrtBeta?: Maybe; + /** The token symbol of the pool as per contract */ + symbol: Scalars['String']; + /** Data specific to gyro pools */ + tauAlphaX?: Maybe; + /** Data specific to gyro pools */ + tauAlphaY?: Maybe; + /** Data specific to gyro pools */ + tauBetaX?: Maybe; + /** Data specific to gyro pools */ + tauBetaY?: Maybe; + /** The pool type, such as weighted, stable, etc. */ + type: GqlPoolType; + /** Data specific to gyro pools */ + u?: Maybe; + /** Data specific to gyro pools */ + v?: Maybe; + /** The version of the pool type. */ + version: Scalars['Int']; + /** Data specific to gyro pools */ + w?: Maybe; + /** Data specific to gyro pools */ + z?: Maybe; +} + export interface GqlPoolApr { __typename?: 'GqlPoolApr'; apr: GqlPoolAprValue; @@ -216,7 +288,14 @@ export interface GqlPoolAprItem { apr: Scalars['Float']; /** The id of the APR item */ id: Scalars['ID']; - /** The title of the APR item, a human readable form */ + /** The reward token address, if the APR originates from token emissions */ + rewardTokenAddress?: Maybe; + /** The reward token symbol, if the APR originates from token emissions */ + rewardTokenSymbol?: Maybe; + /** + * The title of the APR item, a human readable form + * @deprecated No replacement, should be built client side + */ title: Scalars['String']; /** Specific type of this APR */ type: GqlPoolAprItemType; @@ -230,11 +309,13 @@ export type GqlPoolAprItemType = | 'IB_YIELD' /** APR in a pool that can be earned through locking, i.e. veBAL */ | 'LOCKING' + /** Reward APR in a pool from maBEETS emissions allocated by gauge votes. Emitted in BEETS. */ + | 'MABEETS_EMISSIONS' /** Rewards distributed by merkl.xyz */ | 'MERKL' /** Represents if the APR items comes from a nested pool. */ | 'NESTED' - /** Staking reward APR in a pool, i.e. BAL or BEETS. */ + /** Staking reward APR in a pool from a reward token. */ | 'STAKING' /** APR boost that can be earned, i.e. via veBAL or maBEETS. */ | 'STAKING_BOOST' @@ -242,6 +323,8 @@ export type GqlPoolAprItemType = | 'SURPLUS' /** Represents the swap fee APR in a pool. */ | 'SWAP_FEE' + /** Reward APR in a pool from veBAL emissions allocated by gauge votes. Emitted in BAL. */ + | 'VEBAL_EMISSIONS' /** APR that can be earned thourgh voting, i.e. gauge votes */ | 'VOTING'; @@ -2145,6 +2228,8 @@ export interface Query { latestSyncedBlocks: GqlLatestSyncedBlocks; /** Getting swap, add and remove events with paging */ poolEvents: Array; + /** Returns all pools for a given filter, specific for aggregators */ + poolGetAggregatorPools: Array; /** * Will de deprecated in favor of poolEvents * @deprecated Use poolEvents instead @@ -2260,6 +2345,14 @@ export interface QueryPoolEventsArgs { where?: InputMaybe; } +export interface QueryPoolGetAggregatorPoolsArgs { + first?: InputMaybe; + orderBy?: InputMaybe; + orderDirection?: InputMaybe; + skip?: InputMaybe; + where?: InputMaybe; +} + export interface QueryPoolGetBatchSwapsArgs { first?: InputMaybe; skip?: InputMaybe; @@ -2566,6 +2659,7 @@ export type ResolversTypes = ResolversObject<{ GqlLatestSyncedBlocks: ResolverTypeWrapper; GqlNestedPool: ResolverTypeWrapper; GqlPoolAddRemoveEventV3: ResolverTypeWrapper; + GqlPoolAggregator: ResolverTypeWrapper; GqlPoolApr: ResolverTypeWrapper< Omit & { apr: ResolversTypes['GqlPoolAprValue']; @@ -2757,6 +2851,7 @@ export type ResolversParentTypes = ResolversObject<{ GqlLatestSyncedBlocks: GqlLatestSyncedBlocks; GqlNestedPool: GqlNestedPool; GqlPoolAddRemoveEventV3: GqlPoolAddRemoveEventV3; + GqlPoolAggregator: GqlPoolAggregator; GqlPoolApr: Omit & { apr: ResolversParentTypes['GqlPoolAprValue']; nativeRewardApr: ResolversParentTypes['GqlPoolAprValue']; @@ -3050,6 +3145,47 @@ export type GqlPoolAddRemoveEventV3Resolvers< __isTypeOf?: IsTypeOfResolverFn; }>; +export type GqlPoolAggregatorResolvers< + ContextType = Context, + ParentType extends ResolversParentTypes['GqlPoolAggregator'] = ResolversParentTypes['GqlPoolAggregator'], +> = ResolversObject<{ + address?: Resolver; + alpha?: Resolver, ParentType, ContextType>; + amp?: Resolver, ParentType, ContextType>; + beta?: Resolver, ParentType, ContextType>; + c?: Resolver, ParentType, ContextType>; + chain?: Resolver; + createTime?: Resolver; + dSq?: Resolver, ParentType, ContextType>; + decimals?: Resolver; + delta?: Resolver, ParentType, ContextType>; + dynamicData?: Resolver; + epsilon?: Resolver, ParentType, ContextType>; + factory?: Resolver, ParentType, ContextType>; + id?: Resolver; + lambda?: Resolver, ParentType, ContextType>; + name?: Resolver; + owner?: Resolver, ParentType, ContextType>; + poolTokens?: Resolver, ParentType, ContextType>; + protocolVersion?: Resolver; + root3Alpha?: Resolver, ParentType, ContextType>; + s?: Resolver, ParentType, ContextType>; + sqrtAlpha?: Resolver, ParentType, ContextType>; + sqrtBeta?: Resolver, ParentType, ContextType>; + symbol?: Resolver; + tauAlphaX?: Resolver, ParentType, ContextType>; + tauAlphaY?: Resolver, ParentType, ContextType>; + tauBetaX?: Resolver, ParentType, ContextType>; + tauBetaY?: Resolver, ParentType, ContextType>; + type?: Resolver; + u?: Resolver, ParentType, ContextType>; + v?: Resolver, ParentType, ContextType>; + version?: Resolver; + w?: Resolver, ParentType, ContextType>; + z?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}>; + export type GqlPoolAprResolvers< ContextType = Context, ParentType extends ResolversParentTypes['GqlPoolApr'] = ResolversParentTypes['GqlPoolApr'], @@ -3069,6 +3205,8 @@ export type GqlPoolAprItemResolvers< > = ResolversObject<{ apr?: Resolver; id?: Resolver; + rewardTokenAddress?: Resolver, ParentType, ContextType>; + rewardTokenSymbol?: Resolver, ParentType, ContextType>; title?: Resolver; type?: Resolver; __isTypeOf?: IsTypeOfResolverFn; @@ -4812,6 +4950,12 @@ export type QueryResolvers< ContextType, RequireFields >; + poolGetAggregatorPools?: Resolver< + Array, + ParentType, + ContextType, + RequireFields + >; poolGetBatchSwaps?: Resolver< Array, ParentType, @@ -5052,6 +5196,7 @@ export type Resolvers = ResolversObject<{ GqlLatestSyncedBlocks?: GqlLatestSyncedBlocksResolvers; GqlNestedPool?: GqlNestedPoolResolvers; GqlPoolAddRemoveEventV3?: GqlPoolAddRemoveEventV3Resolvers; + GqlPoolAggregator?: GqlPoolAggregatorResolvers; GqlPoolApr?: GqlPoolAprResolvers; GqlPoolAprItem?: GqlPoolAprItemResolvers; GqlPoolAprRange?: GqlPoolAprRangeResolvers; diff --git a/test/mock-viem-client.ts b/test/mock-viem-client.ts new file mode 100644 index 000000000..0129df8f8 --- /dev/null +++ b/test/mock-viem-client.ts @@ -0,0 +1,54 @@ +type MulticallContract = { + address: string; + abi: any; + functionName: string; + args?: any[]; +}; + +export class MockViemClient { + private multicallResults: any[][] = []; + private readContractResults: Map = new Map(); + private multicallIndex: number = 0; + + mockMulticallResult(result: any[]) { + this.multicallResults.push(result); + } + + mockReadContractResult(functionName: string, result: any) { + this.readContractResults.set(functionName, result); + } + + async multicall({ contracts, allowFailure = true }: { contracts: MulticallContract[]; allowFailure: boolean }) { + if (this.multicallIndex >= this.multicallResults.length) { + throw new Error('Not enough mock results provided for multicall'); + } + + const currentResults = this.multicallResults[this.multicallIndex]; + this.multicallIndex++; + + console.log(allowFailure); + if (allowFailure) { + return contracts.map((_, index) => ({ + status: currentResults[index] ? 'success' : 'failure', + result: currentResults[index], + })); + } else { + return currentResults; + } + } + + async readContract({ functionName }: { functionName: string }) { + const result = this.readContractResults.get(functionName); + if (result === undefined) { + throw new Error(`No mock result provided for readContract function: ${functionName}`); + } + return result; + } + + // Reset all mocks + reset() { + this.multicallResults = []; + this.readContractResults.clear(); + this.multicallIndex = 0; + } +}