diff --git a/indexer/src/kadena-server/config/graphql-types.ts b/indexer/src/kadena-server/config/graphql-types.ts index ee4e9c2a..53e42e63 100644 --- a/indexer/src/kadena-server/config/graphql-types.ts +++ b/indexer/src/kadena-server/config/graphql-types.ts @@ -27,6 +27,13 @@ export type Scalars = { Decimal: { input: any; output: any }; }; +export type BalanceNode = { + __typename?: 'BalanceNode'; + balance: Scalars['String']['output']; + chainId: Scalars['String']['output']; + module: Scalars['String']['output']; +}; + /** A unit of information that stores a set of verified transactions. */ export type Block = Node & { __typename?: 'Block'; @@ -704,6 +711,8 @@ export type PoolTransactionsConnection = { export type Query = { __typename?: 'Query'; + /** Retrieve live balances for a given account with optional filtering by chains and module. Default page size is 20. */ + balance: QueryBalanceConnection; /** Retrieve a block by hash. */ block?: Maybe; /** Retrieve blocks by chain and minimal depth. Default page size is 20. */ @@ -786,6 +795,16 @@ export type Query = { transfers: QueryTransfersConnection; }; +export type QueryBalanceArgs = { + accountName: Scalars['String']['input']; + after?: InputMaybe; + before?: InputMaybe; + chainIds?: InputMaybe>; + first?: InputMaybe; + last?: InputMaybe; + module?: InputMaybe; +}; + export type QueryBlockArgs = { hash: Scalars['String']['input']; }; @@ -991,6 +1010,19 @@ export type QueryTransfersArgs = { requestKey?: InputMaybe; }; +/** Connection type for balance query results. */ +export type QueryBalanceConnection = { + __typename?: 'QueryBalanceConnection'; + edges: Array; + pageInfo: PageInfo; +}; + +export type QueryBalanceConnectionEdge = { + __typename?: 'QueryBalanceConnectionEdge'; + cursor: Scalars['String']['output']; + node: BalanceNode; +}; + export type QueryBlocksFromDepthConnection = { __typename?: 'QueryBlocksFromDepthConnection'; edges: Array; @@ -1521,6 +1553,7 @@ export type ResolversInterfaceTypes<_RefType extends Record> = /** Mapping between all available schema types and the resolvers types */ export type ResolversTypes = { + BalanceNode: ResolverTypeWrapper; BigInt: ResolverTypeWrapper; Block: ResolverTypeWrapper< Omit & { @@ -1697,6 +1730,8 @@ export type ResolversTypes = { PoolTransactionType: PoolTransactionType; PoolTransactionsConnection: ResolverTypeWrapper; Query: ResolverTypeWrapper<{}>; + QueryBalanceConnection: ResolverTypeWrapper; + QueryBalanceConnectionEdge: ResolverTypeWrapper; QueryBlocksFromDepthConnection: ResolverTypeWrapper< Omit & { edges: Array; @@ -1822,6 +1857,7 @@ export type ResolversTypes = { /** Mapping between all available schema types and the resolvers parents */ export type ResolversParentTypes = { + BalanceNode: BalanceNode; BigInt: Scalars['BigInt']['output']; Block: Omit & { events: ResolversParentTypes['BlockEventsConnection']; @@ -1968,6 +2004,8 @@ export type ResolversParentTypes = { PoolTransactionEdge: PoolTransactionEdge; PoolTransactionsConnection: PoolTransactionsConnection; Query: {}; + QueryBalanceConnection: QueryBalanceConnection; + QueryBalanceConnectionEdge: QueryBalanceConnectionEdge; QueryBlocksFromDepthConnection: Omit & { edges: Array; }; @@ -2075,6 +2113,16 @@ export type ComplexityDirectiveResolver< Args = ComplexityDirectiveArgs, > = DirectiveResolverFn; +export type BalanceNodeResolvers< + ContextType = any, + ParentType extends ResolversParentTypes['BalanceNode'] = ResolversParentTypes['BalanceNode'], +> = { + balance?: Resolver; + chainId?: Resolver; + module?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export interface BigIntScalarConfig extends GraphQLScalarTypeConfig { name: 'BigInt'; } @@ -2849,6 +2897,12 @@ export type QueryResolvers< ContextType = any, ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query'], > = { + balance?: Resolver< + ResolversTypes['QueryBalanceConnection'], + ParentType, + ContextType, + RequireFields + >; block?: Resolver< Maybe, ParentType, @@ -3025,6 +3079,26 @@ export type QueryResolvers< >; }; +export type QueryBalanceConnectionResolvers< + ContextType = any, + ParentType extends + ResolversParentTypes['QueryBalanceConnection'] = ResolversParentTypes['QueryBalanceConnection'], +> = { + edges?: Resolver, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type QueryBalanceConnectionEdgeResolvers< + ContextType = any, + ParentType extends + ResolversParentTypes['QueryBalanceConnectionEdge'] = ResolversParentTypes['QueryBalanceConnectionEdge'], +> = { + cursor?: Resolver; + node?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type QueryBlocksFromDepthConnectionResolvers< ContextType = any, ParentType extends @@ -3526,6 +3600,7 @@ export type UserGuardResolvers< }; export type Resolvers = { + BalanceNode?: BalanceNodeResolvers; BigInt?: GraphQLScalarType; Block?: BlockResolvers; BlockEventsConnection?: BlockEventsConnectionResolvers; @@ -3581,6 +3656,8 @@ export type Resolvers = { PoolTransactionEdge?: PoolTransactionEdgeResolvers; PoolTransactionsConnection?: PoolTransactionsConnectionResolvers; Query?: QueryResolvers; + QueryBalanceConnection?: QueryBalanceConnectionResolvers; + QueryBalanceConnectionEdge?: QueryBalanceConnectionEdgeResolvers; QueryBlocksFromDepthConnection?: QueryBlocksFromDepthConnectionResolvers; QueryBlocksFromDepthConnectionEdge?: QueryBlocksFromDepthConnectionEdgeResolvers; QueryBlocksFromHeightConnection?: QueryBlocksFromHeightConnectionResolvers; diff --git a/indexer/src/kadena-server/config/schema.graphql b/indexer/src/kadena-server/config/schema.graphql index 2a4814da..d361593f 100644 --- a/indexer/src/kadena-server/config/schema.graphql +++ b/indexer/src/kadena-server/config/schema.graphql @@ -120,6 +120,19 @@ type Subscription { } type Query { + """ + Retrieve live balances for a given account with optional filtering by chains and module. Default page size is 20. + """ + balance( + accountName: String! + chainIds: [String!] + module: String + after: String + before: String + first: Int + last: Int + ): QueryBalanceConnection! @complexity(value: 1, multipliers: ["first", "last"]) + """ Retrieve a block by hash. """ @@ -419,6 +432,25 @@ type Query { tokenPrices(protocolAddress: String): [TokenPrice!]! @complexity(value: 1) } +""" +Connection type for balance query results. +""" +type QueryBalanceConnection { + edges: [QueryBalanceConnectionEdge!]! + pageInfo: PageInfo! +} + +type QueryBalanceConnectionEdge { + cursor: String! + node: BalanceNode! +} + +type BalanceNode { + module: String! + chainId: String! + balance: String! +} + """ A fungible-specific account. """ diff --git a/indexer/src/kadena-server/repository/application/balance-repository.ts b/indexer/src/kadena-server/repository/application/balance-repository.ts index 0e8c5e62..e78ca9ac 100644 --- a/indexer/src/kadena-server/repository/application/balance-repository.ts +++ b/indexer/src/kadena-server/repository/application/balance-repository.ts @@ -95,6 +95,15 @@ export type TokenOutput = Token; */ export interface GetTokensParams extends PaginationsParams {} +/** + * Parameters for fetching account balances with pagination and filters. + */ +export interface GetAccountBalancesParams extends PaginationsParams { + accountName: string; + chainIds?: string[] | null; + module?: string | null; +} + /** * Interface defining the contract for balance data access. * Implementations of this interface handle the details of retrieving @@ -154,6 +163,14 @@ export default interface BalanceRepository { edges: ConnectionEdge[]; }>; + /** + * Retrieves live balances for an account with optional module and chain filters, paginated. + */ + getAccountBalances(params: GetAccountBalancesParams): Promise<{ + pageInfo: PageInfo; + edges: ConnectionEdge<{ module: string; chainId: string; balance: string }>[]; + }>; + /** * Retrieves fungible token information for a specific account directly from a blockchain node. * This provides real-time balance information rather than indexed data. diff --git a/indexer/src/kadena-server/repository/infra/repository/balance-db-repository.ts b/indexer/src/kadena-server/repository/infra/repository/balance-db-repository.ts index e28ad000..3407741a 100644 --- a/indexer/src/kadena-server/repository/infra/repository/balance-db-repository.ts +++ b/indexer/src/kadena-server/repository/infra/repository/balance-db-repository.ts @@ -467,6 +467,98 @@ export default class BalanceDbRepository implements BalanceRepository { return pageInfo; } + /** + * Retrieves live balances for an account with optional module and chain filters, paginated. + */ + async getAccountBalances(params: { + accountName: string; + chainIds?: string[] | null; + module?: string | null; + after?: string | null; + before?: string | null; + first?: number | null; + last?: number | null; + }) { + const { accountName, chainIds, module } = params; + const { limit, order, after, before } = getPaginationParams(params); + + const queryParams: any[] = [accountName, limit]; + let conditions = 'WHERE b.account = $1'; + + if (module) { + queryParams.push(module); + conditions += ` AND b.module = $${queryParams.length}`; + } + + if (chainIds && chainIds.length) { + queryParams.push(chainIds); + conditions += ` AND b."chainId" = ANY($${queryParams.length})`; + } + + if (after) { + queryParams.push(after); + conditions += ` AND b.id < $${queryParams.length}`; + } + + if (before) { + queryParams.push(before); + conditions += ` AND b.id > $${queryParams.length}`; + } + + // If module specified, return at most one row per chain (latest by id) + let query = ''; + if (module) { + query = ` + WITH ranked AS ( + SELECT b.id, b."chainId", b.module, b.account, + ROW_NUMBER() OVER (PARTITION BY b."chainId" ORDER BY b.id DESC) AS rn + FROM "Balances" b + ${conditions} + ) + SELECT id, "chainId", module, account + FROM ranked + WHERE rn = 1 + ORDER BY id ${order} + LIMIT $2 + `; + } else { + // Without module, allow multiple modules per chain + query = ` + SELECT b.id, b."chainId", b.module, b.account + FROM "Balances" b + ${conditions} + ORDER BY b.id ${order} + LIMIT $2 + `; + } + + const { rows } = await rootPgPool.query(query, queryParams); + + // Query node for balances in parallel + const nodeQueries = rows.map(async row => { + const res = await handleSingleQuery({ + chainId: String(row.chainId), + code: `(${row.module}.details \"${row.account}\")`, + }); + const balance = formatBalance_NODE(res).toString(); + return { + cursor: row.id.toString(), + node: { module: row.module, chainId: String(row.chainId), balance }, + }; + }); + + const edges = await Promise.all(nodeQueries); + const { pageInfo, edges: edgesProcessed } = getPageInfo({ + order, + limit, + edges, + after, + before, + }); + + return { pageInfo, edges: edgesProcessed }; + } + private async processAccounts( rows: { account: string; chainId: string }[], fungibleName: string, diff --git a/indexer/src/kadena-server/resolvers/index.ts b/indexer/src/kadena-server/resolvers/index.ts index c2e87ab6..ea3be40c 100644 --- a/indexer/src/kadena-server/resolvers/index.ts +++ b/indexer/src/kadena-server/resolvers/index.ts @@ -68,6 +68,7 @@ import { fungibleAccountsByPublicKeyQueryResolver } from './query/fungible-accou import { fungibleChainAccountQueryResolver } from './query/fungible-chain-account-query-resolver'; import { fungibleChainAccountsByPublicKeyQueryResolver } from './query/fungible-chain-accounts-by-public-key-query-resolver'; import { fungibleChainAccountsQueryResolver } from './query/fungible-chain-accounts-query-resolver'; +import { balanceQueryResolver } from './query/balance-query-resolver'; import { gasLimitEstimateQueryResolver } from './query/gas-limit-estimate-query-resolver'; import { graphConfigurationQueryResolver } from './query/graph-configuration-query-resolver'; import { lastBlockHeightQueryResolver } from './query/last-block-height-query-resolver'; @@ -127,6 +128,7 @@ export const resolvers: Resolvers = { events: eventsSubscriptionResolver, }, Query: { + balance: balanceQueryResolver, block: blockQueryResolver, blocksFromDepth: blocksFromDepthQueryResolver, blocksFromHeight: blocksFromHeightQueryResolver, diff --git a/indexer/src/kadena-server/resolvers/query/balance-query-resolver.ts b/indexer/src/kadena-server/resolvers/query/balance-query-resolver.ts new file mode 100644 index 00000000..8107ed5c --- /dev/null +++ b/indexer/src/kadena-server/resolvers/query/balance-query-resolver.ts @@ -0,0 +1,20 @@ +import { ResolverContext } from '../../config/apollo-server-config'; +import { QueryResolvers } from '../../config/graphql-types'; + +export const balanceQueryResolver: QueryResolvers['balance'] = async ( + _parent, + args, + context, +) => { + const { accountName, chainIds, module, after, before, first, last } = args; + const output = await context.balanceRepository.getAccountBalances({ + accountName, + chainIds: chainIds ?? null, + module: module ?? null, + after: after ?? null, + before: before ?? null, + first: first ?? null, + last: last ?? null, + }); + return output; +};