From 054be7dfc14ac0148d350ebd7b2bd6a9231ef193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pr=C3=A9vost?= <998369+prevostc@users.noreply.github.com> Date: Thu, 19 Dec 2024 17:39:06 +0100 Subject: [PATCH] Update top-holder endpoint --- README.md | 4 +- src/queries/LatestVaultSharesBalances.graphql | 32 ------ src/queries/VaultSharesBalances.graphql | 8 +- src/routes/v1/contract.ts | 99 ++++++++++++++++++- src/routes/v1/vault.ts | 94 +----------------- 5 files changed, 107 insertions(+), 130 deletions(-) delete mode 100644 src/queries/LatestVaultSharesBalances.graphql diff --git a/README.md b/README.md index b6bcb00..ef19ce4 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ http://localhost:4000/api/v1/status https://balance-api.beefy.finance/api/v1/holders/counts/all http://localhost:4000/api/v1/holders/counts/all -https://balance-api.beefy.finance/api/v1/vault/arbitrum/top-holders?vault_addresses=0x0481ad5b536139472af5ce692330dbf00bbd8672&vault_addresses=0x0d1f71170d93121b48a9e8fc7400e8e6a6821500&limit=10 -http://localhost:4000/api/v1/vault/arbitrum/top-holders?vault_addresses=0x0481ad5b536139472af5ce692330dbf00bbd8672&vault_addresses=0x0d1f71170d93121b48a9e8fc7400e8e6a6821500&limit=10 +https://balance-api.beefy.finance/api/v1/contract/arbitrum/top-holders?contract_addresses=0x0481ad5b536139472af5ce692330dbf00bbd8672&contract_addresses=0x0d1f71170d93121b48a9e8fc7400e8e6a6821500&limit=10 +http://localhost:4000/api/v1/contract/arbitrum/top-holders?contract_addresses=0x0481ad5b536139472af5ce692330dbf00bbd8672&contract_addresses=0x0d1f71170d93121b48a9e8fc7400e8e6a6821500&limit=10 https://balance-api.beefy.finance/api/v1/vault/base/baseswap-cow-weth-cbbtc/20449610/share-tokens-balances http://localhost:4000/api/v1/vault/base/baseswap-cow-weth-cbbtc/20449610/share-tokens-balances diff --git a/src/queries/LatestVaultSharesBalances.graphql b/src/queries/LatestVaultSharesBalances.graphql deleted file mode 100644 index 5a98f34..0000000 --- a/src/queries/LatestVaultSharesBalances.graphql +++ /dev/null @@ -1,32 +0,0 @@ -query LatestVaultSharesBalances( - $token_in_1: [Bytes!]! - $token_in_2: [String!]! - $account_not_in: [String!]! - $tokenFirst: Int = 1000 - $tokenSkip: Int = 0 - $first: Int = 1000 - $skip: Int = 0 -) { - tokens(first: $tokenFirst, skip: $tokenSkip, where: { id_in: $token_in_1 }) { - id - name - decimals - symbol - - balances( - orderBy: id - orderDirection: desc - first: $first - skip: $skip - where: { - amount_gt: 0 - account_not_in: $account_not_in - } - ) { - account { - id - } - amount - } - } -} diff --git a/src/queries/VaultSharesBalances.graphql b/src/queries/VaultSharesBalances.graphql index 9814824..c9da62d 100644 --- a/src/queries/VaultSharesBalances.graphql +++ b/src/queries/VaultSharesBalances.graphql @@ -1,4 +1,4 @@ -query VaultSharesBalances( +query TokenBalance( $token_in_1: [Bytes!]! $token_in_2: [String!]! $account_not_in: [String!]! @@ -7,6 +7,8 @@ query VaultSharesBalances( $tokenSkip: Int = 0 $first: Int = 1000 $skip: Int = 0 + $orderBy: TokenBalance_orderBy = id + $orderDirection: OrderDirection = desc ) { tokens(first: $tokenFirst, skip: $tokenSkip, where: { id_in: $token_in_1 }) { id @@ -15,8 +17,8 @@ query VaultSharesBalances( symbol balances( - orderBy: id - orderDirection: desc + orderBy: $orderBy + orderDirection: $orderDirection first: $first skip: $skip where: { diff --git a/src/routes/v1/contract.ts b/src/routes/v1/contract.ts index 664c3b1..3a30fa1 100644 --- a/src/routes/v1/contract.ts +++ b/src/routes/v1/contract.ts @@ -3,6 +3,7 @@ import type { FastifyInstance, FastifyPluginOptions, FastifySchema } from 'fasti import { min } from 'lodash'; import type { Hex } from 'viem'; import { type ChainId, chainIdSchema } from '../../config/chains'; +import { OrderDirection, TokenBalance_OrderBy } from '../../queries/codegen/sdk'; import { addressSchema } from '../../schema/address'; import { bigintSchema } from '../../schema/bigint'; import { getAsyncCache } from '../../utils/async-lock'; @@ -65,6 +66,53 @@ export default async function ( ); } + // all holder count list for all chains + { + const urlParamsSchema = Type.Object({ + chain: chainIdSchema, + }); + type UrlParams = Static; + + const querySchema = Type.Object({ + contract_addresses: Type.Array(addressSchema, { minItems: 1, maxItems: 100 }), + limit: Type.Number({ default: 100, minimum: 1, maximum: 1000 }), + }); + type QueryParams = Static; + + const schema: FastifySchema = { + tags: ['contract'], + params: urlParamsSchema, + querystring: querySchema, + response: { + 200: contractHoldersSchema, + }, + }; + + instance.get<{ Params: UrlParams; Querystring: QueryParams }>( + '/:chain/top-holders', + { schema }, + async (request, reply) => { + const { chain } = request.params; + const { contract_addresses, limit } = request.query; + + if ( + !contract_addresses || + !Array.isArray(contract_addresses) || + contract_addresses.length === 0 + ) { + throw new Error('contract_addresses is required'); + } + + const result = await asyncCache.wrap( + `vault:${chain}:${contract_addresses.join(',')}:top-holders:${limit}`, + 5 * 60 * 1000, + async () => await getTopHolders(chain, contract_addresses as Hex[], limit) + ); + reply.send(result); + } + ); + } + done(); } @@ -91,7 +139,7 @@ const getContractHolders = async ( fetchPage: ({ skip: tokenSkip, first: tokenFirst }) => paginate({ fetchPage: ({ skip, first }) => - sdk.VaultSharesBalances({ + sdk.TokenBalance({ tokenSkip, tokenFirst, skip, @@ -136,3 +184,52 @@ const getContractHolders = async ( ) ); }; + +const getTopHolders = async (chainId: ChainId, vault_addresses: Hex[], limit: number) => { + const res = ( + await Promise.all( + getSdksForChain(chainId).map(sdk => + paginate({ + fetchPage: ({ skip: tokenSkip, first: tokenFirst }) => + sdk.TokenBalance({ + tokenSkip, + tokenFirst, + skip: 0, + first: limit, + account_not_in: ['0x0000000000000000000000000000000000000000'], // providing an empty account_not_in will return 0 holders + token_in_1: vault_addresses, + token_in_2: vault_addresses, + orderBy: TokenBalance_OrderBy.Amount, + orderDirection: OrderDirection.Desc, + }), + count: res => res.data.tokens.length, + }) + ) + ) + ).flat(); + + return res.flatMap(chainRes => + chainRes.data.tokens.map(token => { + if (!token.symbol) { + throw new Error(`Token ${token.id} has no symbol`); + } + if (!token.decimals) { + throw new Error(`Token ${token.id} has no decimals`); + } + if (!token.name) { + throw new Error(`Token ${token.id} has no name`); + } + + return { + id: token.id, + name: token.name, + symbol: token.symbol, + decimals: Number.parseInt(token.decimals, 10), + balances: token.balances.map(balance => ({ + balance: balance.amount, + holder: balance.account.id, + })), + }; + }) + ); +}; diff --git a/src/routes/v1/vault.ts b/src/routes/v1/vault.ts index 3f11385..5e0ae8d 100644 --- a/src/routes/v1/vault.ts +++ b/src/routes/v1/vault.ts @@ -19,49 +19,6 @@ export default async function ( ) { const asyncCache = getAsyncCache(); - // all holder count list for all chains - { - const urlParamsSchema = Type.Object({ - chain: chainIdSchema, - }); - type UrlParams = Static; - - const querySchema = Type.Object({ - vault_addresses: Type.Array(addressSchema, { minItems: 1, maxItems: 100 }), - limit: Type.Number({ default: 100, minimum: 1, maximum: 1000 }), - }); - type QueryParams = Static; - - const schema: FastifySchema = { - tags: ['vault'], - params: urlParamsSchema, - querystring: querySchema, - response: { - 200: vaultHoldersSchema, - }, - }; - - instance.get<{ Params: UrlParams; Querystring: QueryParams }>( - '/:chain/top-holders', - { schema }, - async (request, reply) => { - const { chain } = request.params; - const { vault_addresses, limit } = request.query; - - if (!vault_addresses || !Array.isArray(vault_addresses) || vault_addresses.length === 0) { - throw new Error('vault_addresses is required'); - } - - const result = await asyncCache.wrap( - `vault:${chain}:${vault_addresses.join(',')}:top-holders:${limit}`, - 5 * 60 * 1000, - async () => await getTopHolders(chain, vault_addresses as Hex[], limit) - ); - reply.send(result); - } - ); - } - // all holder count list for all chains { const urlParamsSchema = Type.Object({ @@ -294,7 +251,7 @@ const getVaultHolders = async ( fetchPage: ({ skip: tokenSkip, first: tokenFirst }) => paginate({ fetchPage: ({ skip, first }) => - sdk.VaultSharesBalances({ + sdk.TokenBalance({ tokenSkip, tokenFirst, skip, @@ -435,7 +392,7 @@ const _getVaultHoldersAsBaseVaultEquivalent = async ( fetchPage: ({ skip: tokenSkip, first: tokenFirst }) => paginate({ fetchPage: ({ skip, first }) => - sdk.VaultSharesBalances({ + sdk.TokenBalance({ tokenSkip, tokenFirst, skip, @@ -639,50 +596,3 @@ const _getVaultHoldersAsBaseVaultEquivalent = async ( })); return mergedHolders; }; - -const getTopHolders = async (chainId: ChainId, vault_addresses: Hex[], limit: number) => { - const res = ( - await Promise.all( - getSdksForChain(chainId).map(sdk => - paginate({ - fetchPage: ({ skip: tokenSkip, first: tokenFirst }) => - sdk.VaultSharesBalances({ - tokenSkip, - tokenFirst, - skip: 0, - first: limit, - account_not_in: ['0x0000000000000000000000000000000000000000'], // providing an empty account_not_in will return 0 holders - token_in_1: vault_addresses, - token_in_2: vault_addresses, - }), - count: res => res.data.tokens.length, - }) - ) - ) - ).flat(); - - return res.flatMap(chainRes => - chainRes.data.tokens.map(token => { - if (!token.symbol) { - throw new Error(`Token ${token.id} has no symbol`); - } - if (!token.decimals) { - throw new Error(`Token ${token.id} has no decimals`); - } - if (!token.name) { - throw new Error(`Token ${token.id} has no name`); - } - - return { - id: token.id, - name: token.name, - symbol: token.symbol, - decimals: Number.parseInt(token.decimals, 10), - balances: token.balances.map(balance => ({ - balance: balance.amount, - holder: balance.account.id, - })), - }; - }) - ); -};