Skip to content

Commit 9c448cf

Browse files
committed
feat: balances queries
1 parent 123160e commit 9c448cf

File tree

6 files changed

+240
-0
lines changed

6 files changed

+240
-0
lines changed

indexer/src/kadena-server/config/graphql-types.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ export type Scalars = {
2727
Decimal: { input: any; output: any };
2828
};
2929

30+
export type BalanceNode = {
31+
__typename?: 'BalanceNode';
32+
balance: Scalars['String']['output'];
33+
chainId: Scalars['String']['output'];
34+
module: Scalars['String']['output'];
35+
};
36+
3037
/** A unit of information that stores a set of verified transactions. */
3138
export type Block = Node & {
3239
__typename?: 'Block';
@@ -704,6 +711,8 @@ export type PoolTransactionsConnection = {
704711

705712
export type Query = {
706713
__typename?: 'Query';
714+
/** Retrieve live balances for a given account with optional filtering by chains and module. Default page size is 20. */
715+
balance: QueryBalanceConnection;
707716
/** Retrieve a block by hash. */
708717
block?: Maybe<Block>;
709718
/** Retrieve blocks by chain and minimal depth. Default page size is 20. */
@@ -786,6 +795,16 @@ export type Query = {
786795
transfers: QueryTransfersConnection;
787796
};
788797

798+
export type QueryBalanceArgs = {
799+
accountName: Scalars['String']['input'];
800+
after?: InputMaybe<Scalars['String']['input']>;
801+
before?: InputMaybe<Scalars['String']['input']>;
802+
chainIds?: InputMaybe<Array<Scalars['String']['input']>>;
803+
first?: InputMaybe<Scalars['Int']['input']>;
804+
last?: InputMaybe<Scalars['Int']['input']>;
805+
module?: InputMaybe<Scalars['String']['input']>;
806+
};
807+
789808
export type QueryBlockArgs = {
790809
hash: Scalars['String']['input'];
791810
};
@@ -991,6 +1010,19 @@ export type QueryTransfersArgs = {
9911010
requestKey?: InputMaybe<Scalars['String']['input']>;
9921011
};
9931012

1013+
/** Connection type for balance query results. */
1014+
export type QueryBalanceConnection = {
1015+
__typename?: 'QueryBalanceConnection';
1016+
edges: Array<QueryBalanceConnectionEdge>;
1017+
pageInfo: PageInfo;
1018+
};
1019+
1020+
export type QueryBalanceConnectionEdge = {
1021+
__typename?: 'QueryBalanceConnectionEdge';
1022+
cursor: Scalars['String']['output'];
1023+
node: BalanceNode;
1024+
};
1025+
9941026
export type QueryBlocksFromDepthConnection = {
9951027
__typename?: 'QueryBlocksFromDepthConnection';
9961028
edges: Array<QueryBlocksFromDepthConnectionEdge>;
@@ -1521,6 +1553,7 @@ export type ResolversInterfaceTypes<_RefType extends Record<string, unknown>> =
15211553

15221554
/** Mapping between all available schema types and the resolvers types */
15231555
export type ResolversTypes = {
1556+
BalanceNode: ResolverTypeWrapper<BalanceNode>;
15241557
BigInt: ResolverTypeWrapper<Scalars['BigInt']['output']>;
15251558
Block: ResolverTypeWrapper<
15261559
Omit<Block, 'events' | 'minerAccount' | 'parent' | 'transactions'> & {
@@ -1697,6 +1730,8 @@ export type ResolversTypes = {
16971730
PoolTransactionType: PoolTransactionType;
16981731
PoolTransactionsConnection: ResolverTypeWrapper<PoolTransactionsConnection>;
16991732
Query: ResolverTypeWrapper<{}>;
1733+
QueryBalanceConnection: ResolverTypeWrapper<QueryBalanceConnection>;
1734+
QueryBalanceConnectionEdge: ResolverTypeWrapper<QueryBalanceConnectionEdge>;
17001735
QueryBlocksFromDepthConnection: ResolverTypeWrapper<
17011736
Omit<QueryBlocksFromDepthConnection, 'edges'> & {
17021737
edges: Array<ResolversTypes['QueryBlocksFromDepthConnectionEdge']>;
@@ -1822,6 +1857,7 @@ export type ResolversTypes = {
18221857

18231858
/** Mapping between all available schema types and the resolvers parents */
18241859
export type ResolversParentTypes = {
1860+
BalanceNode: BalanceNode;
18251861
BigInt: Scalars['BigInt']['output'];
18261862
Block: Omit<Block, 'events' | 'minerAccount' | 'parent' | 'transactions'> & {
18271863
events: ResolversParentTypes['BlockEventsConnection'];
@@ -1968,6 +2004,8 @@ export type ResolversParentTypes = {
19682004
PoolTransactionEdge: PoolTransactionEdge;
19692005
PoolTransactionsConnection: PoolTransactionsConnection;
19702006
Query: {};
2007+
QueryBalanceConnection: QueryBalanceConnection;
2008+
QueryBalanceConnectionEdge: QueryBalanceConnectionEdge;
19712009
QueryBlocksFromDepthConnection: Omit<QueryBlocksFromDepthConnection, 'edges'> & {
19722010
edges: Array<ResolversParentTypes['QueryBlocksFromDepthConnectionEdge']>;
19732011
};
@@ -2075,6 +2113,16 @@ export type ComplexityDirectiveResolver<
20752113
Args = ComplexityDirectiveArgs,
20762114
> = DirectiveResolverFn<Result, Parent, ContextType, Args>;
20772115

2116+
export type BalanceNodeResolvers<
2117+
ContextType = any,
2118+
ParentType extends ResolversParentTypes['BalanceNode'] = ResolversParentTypes['BalanceNode'],
2119+
> = {
2120+
balance?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2121+
chainId?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2122+
module?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2123+
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
2124+
};
2125+
20782126
export interface BigIntScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['BigInt'], any> {
20792127
name: 'BigInt';
20802128
}
@@ -2849,6 +2897,12 @@ export type QueryResolvers<
28492897
ContextType = any,
28502898
ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query'],
28512899
> = {
2900+
balance?: Resolver<
2901+
ResolversTypes['QueryBalanceConnection'],
2902+
ParentType,
2903+
ContextType,
2904+
RequireFields<QueryBalanceArgs, 'accountName'>
2905+
>;
28522906
block?: Resolver<
28532907
Maybe<ResolversTypes['Block']>,
28542908
ParentType,
@@ -3025,6 +3079,26 @@ export type QueryResolvers<
30253079
>;
30263080
};
30273081

3082+
export type QueryBalanceConnectionResolvers<
3083+
ContextType = any,
3084+
ParentType extends
3085+
ResolversParentTypes['QueryBalanceConnection'] = ResolversParentTypes['QueryBalanceConnection'],
3086+
> = {
3087+
edges?: Resolver<Array<ResolversTypes['QueryBalanceConnectionEdge']>, ParentType, ContextType>;
3088+
pageInfo?: Resolver<ResolversTypes['PageInfo'], ParentType, ContextType>;
3089+
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
3090+
};
3091+
3092+
export type QueryBalanceConnectionEdgeResolvers<
3093+
ContextType = any,
3094+
ParentType extends
3095+
ResolversParentTypes['QueryBalanceConnectionEdge'] = ResolversParentTypes['QueryBalanceConnectionEdge'],
3096+
> = {
3097+
cursor?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
3098+
node?: Resolver<ResolversTypes['BalanceNode'], ParentType, ContextType>;
3099+
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
3100+
};
3101+
30283102
export type QueryBlocksFromDepthConnectionResolvers<
30293103
ContextType = any,
30303104
ParentType extends
@@ -3526,6 +3600,7 @@ export type UserGuardResolvers<
35263600
};
35273601

35283602
export type Resolvers<ContextType = any> = {
3603+
BalanceNode?: BalanceNodeResolvers<ContextType>;
35293604
BigInt?: GraphQLScalarType;
35303605
Block?: BlockResolvers<ContextType>;
35313606
BlockEventsConnection?: BlockEventsConnectionResolvers<ContextType>;
@@ -3581,6 +3656,8 @@ export type Resolvers<ContextType = any> = {
35813656
PoolTransactionEdge?: PoolTransactionEdgeResolvers<ContextType>;
35823657
PoolTransactionsConnection?: PoolTransactionsConnectionResolvers<ContextType>;
35833658
Query?: QueryResolvers<ContextType>;
3659+
QueryBalanceConnection?: QueryBalanceConnectionResolvers<ContextType>;
3660+
QueryBalanceConnectionEdge?: QueryBalanceConnectionEdgeResolvers<ContextType>;
35843661
QueryBlocksFromDepthConnection?: QueryBlocksFromDepthConnectionResolvers<ContextType>;
35853662
QueryBlocksFromDepthConnectionEdge?: QueryBlocksFromDepthConnectionEdgeResolvers<ContextType>;
35863663
QueryBlocksFromHeightConnection?: QueryBlocksFromHeightConnectionResolvers<ContextType>;

indexer/src/kadena-server/config/schema.graphql

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,19 @@ type Subscription {
120120
}
121121

122122
type Query {
123+
"""
124+
Retrieve live balances for a given account with optional filtering by chains and module. Default page size is 20.
125+
"""
126+
balance(
127+
accountName: String!
128+
chainIds: [String!]
129+
module: String
130+
after: String
131+
before: String
132+
first: Int
133+
last: Int
134+
): QueryBalanceConnection! @complexity(value: 1, multipliers: ["first", "last"])
135+
123136
"""
124137
Retrieve a block by hash.
125138
"""
@@ -419,6 +432,25 @@ type Query {
419432
tokenPrices(protocolAddress: String): [TokenPrice!]! @complexity(value: 1)
420433
}
421434

435+
"""
436+
Connection type for balance query results.
437+
"""
438+
type QueryBalanceConnection {
439+
edges: [QueryBalanceConnectionEdge!]!
440+
pageInfo: PageInfo!
441+
}
442+
443+
type QueryBalanceConnectionEdge {
444+
cursor: String!
445+
node: BalanceNode!
446+
}
447+
448+
type BalanceNode {
449+
module: String!
450+
chainId: String!
451+
balance: String!
452+
}
453+
422454
"""
423455
A fungible-specific account.
424456
"""

indexer/src/kadena-server/repository/application/balance-repository.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,15 @@ export type TokenOutput = Token;
9595
*/
9696
export interface GetTokensParams extends PaginationsParams {}
9797

98+
/**
99+
* Parameters for fetching account balances with pagination and filters.
100+
*/
101+
export interface GetAccountBalancesParams extends PaginationsParams {
102+
accountName: string;
103+
chainIds?: string[] | null;
104+
module?: string | null;
105+
}
106+
98107
/**
99108
* Interface defining the contract for balance data access.
100109
* Implementations of this interface handle the details of retrieving
@@ -154,6 +163,14 @@ export default interface BalanceRepository {
154163
edges: ConnectionEdge<TokenOutput>[];
155164
}>;
156165

166+
/**
167+
* Retrieves live balances for an account with optional module and chain filters, paginated.
168+
*/
169+
getAccountBalances(params: GetAccountBalancesParams): Promise<{
170+
pageInfo: PageInfo;
171+
edges: ConnectionEdge<{ module: string; chainId: string; balance: string }>[];
172+
}>;
173+
157174
/**
158175
* Retrieves fungible token information for a specific account directly from a blockchain node.
159176
* This provides real-time balance information rather than indexed data.

indexer/src/kadena-server/repository/infra/repository/balance-db-repository.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,98 @@ export default class BalanceDbRepository implements BalanceRepository {
467467
return pageInfo;
468468
}
469469

470+
/**
471+
* Retrieves live balances for an account with optional module and chain filters, paginated.
472+
*/
473+
async getAccountBalances(params: {
474+
accountName: string;
475+
chainIds?: string[] | null;
476+
module?: string | null;
477+
after?: string | null;
478+
before?: string | null;
479+
first?: number | null;
480+
last?: number | null;
481+
}) {
482+
const { accountName, chainIds, module } = params;
483+
const { limit, order, after, before } = getPaginationParams(params);
484+
485+
const queryParams: any[] = [accountName, limit];
486+
let conditions = 'WHERE b.account = $1';
487+
488+
if (module) {
489+
queryParams.push(module);
490+
conditions += ` AND b.module = $${queryParams.length}`;
491+
}
492+
493+
if (chainIds && chainIds.length) {
494+
queryParams.push(chainIds);
495+
conditions += ` AND b."chainId" = ANY($${queryParams.length})`;
496+
}
497+
498+
if (after) {
499+
queryParams.push(after);
500+
conditions += ` AND b.id < $${queryParams.length}`;
501+
}
502+
503+
if (before) {
504+
queryParams.push(before);
505+
conditions += ` AND b.id > $${queryParams.length}`;
506+
}
507+
508+
// If module specified, return at most one row per chain (latest by id)
509+
let query = '';
510+
if (module) {
511+
query = `
512+
WITH ranked AS (
513+
SELECT b.id, b."chainId", b.module, b.account,
514+
ROW_NUMBER() OVER (PARTITION BY b."chainId" ORDER BY b.id DESC) AS rn
515+
FROM "Balances" b
516+
${conditions}
517+
)
518+
SELECT id, "chainId", module, account
519+
FROM ranked
520+
WHERE rn = 1
521+
ORDER BY id ${order}
522+
LIMIT $2
523+
`;
524+
} else {
525+
// Without module, allow multiple modules per chain
526+
query = `
527+
SELECT b.id, b."chainId", b.module, b.account
528+
FROM "Balances" b
529+
${conditions}
530+
ORDER BY b.id ${order}
531+
LIMIT $2
532+
`;
533+
}
534+
535+
const { rows } = await rootPgPool.query(query, queryParams);
536+
537+
// Query node for balances in parallel
538+
const nodeQueries = rows.map(async row => {
539+
const res = await handleSingleQuery({
540+
chainId: String(row.chainId),
541+
code: `(${row.module}.details \"${row.account}\")`,
542+
});
543+
const balance = formatBalance_NODE(res).toString();
544+
return {
545+
cursor: row.id.toString(),
546+
node: { module: row.module, chainId: String(row.chainId), balance },
547+
};
548+
});
549+
550+
const edges = await Promise.all(nodeQueries);
551+
const { pageInfo, edges: edgesProcessed } = getPageInfo({
552+
order,
553+
limit,
554+
edges,
555+
after,
556+
before,
557+
});
558+
559+
return { pageInfo, edges: edgesProcessed };
560+
}
561+
470562
private async processAccounts(
471563
rows: { account: string; chainId: string }[],
472564
fungibleName: string,

indexer/src/kadena-server/resolvers/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import { fungibleAccountsByPublicKeyQueryResolver } from './query/fungible-accou
6868
import { fungibleChainAccountQueryResolver } from './query/fungible-chain-account-query-resolver';
6969
import { fungibleChainAccountsByPublicKeyQueryResolver } from './query/fungible-chain-accounts-by-public-key-query-resolver';
7070
import { fungibleChainAccountsQueryResolver } from './query/fungible-chain-accounts-query-resolver';
71+
import { balanceQueryResolver } from './query/balance-query-resolver';
7172
import { gasLimitEstimateQueryResolver } from './query/gas-limit-estimate-query-resolver';
7273
import { graphConfigurationQueryResolver } from './query/graph-configuration-query-resolver';
7374
import { lastBlockHeightQueryResolver } from './query/last-block-height-query-resolver';
@@ -127,6 +128,7 @@ export const resolvers: Resolvers<ResolverContext> = {
127128
events: eventsSubscriptionResolver,
128129
},
129130
Query: {
131+
balance: balanceQueryResolver,
130132
block: blockQueryResolver,
131133
blocksFromDepth: blocksFromDepthQueryResolver,
132134
blocksFromHeight: blocksFromHeightQueryResolver,
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ResolverContext } from '../../config/apollo-server-config';
2+
import { QueryResolvers } from '../../config/graphql-types';
3+
4+
export const balanceQueryResolver: QueryResolvers<ResolverContext>['balance'] = async (
5+
_parent,
6+
args,
7+
context,
8+
) => {
9+
const { accountName, chainIds, module, after, before, first, last } = args;
10+
const output = await context.balanceRepository.getAccountBalances({
11+
accountName,
12+
chainIds: chainIds ?? null,
13+
module: module ?? null,
14+
after: after ?? null,
15+
before: before ?? null,
16+
first: first ?? null,
17+
last: last ?? null,
18+
});
19+
return output;
20+
};

0 commit comments

Comments
 (0)