Skip to content

Commit a598c54

Browse files
authored
Merge pull request #437 from hack-a-chain-software/feat/query-balances
feat: balances queries
2 parents 7598c02 + 9c448cf commit a598c54

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';
@@ -713,6 +720,8 @@ export type PoolTransactionsConnection = {
713720

714721
export type Query = {
715722
__typename?: 'Query';
723+
/** Retrieve live balances for a given account with optional filtering by chains and module. Default page size is 20. */
724+
balance: QueryBalanceConnection;
716725
/** Retrieve a block by hash. */
717726
block?: Maybe<Block>;
718727
/** Retrieve blocks by chain and minimal depth. Default page size is 20. */
@@ -795,6 +804,16 @@ export type Query = {
795804
transfers: QueryTransfersConnection;
796805
};
797806

807+
export type QueryBalanceArgs = {
808+
accountName: Scalars['String']['input'];
809+
after?: InputMaybe<Scalars['String']['input']>;
810+
before?: InputMaybe<Scalars['String']['input']>;
811+
chainIds?: InputMaybe<Array<Scalars['String']['input']>>;
812+
first?: InputMaybe<Scalars['Int']['input']>;
813+
last?: InputMaybe<Scalars['Int']['input']>;
814+
module?: InputMaybe<Scalars['String']['input']>;
815+
};
816+
798817
export type QueryBlockArgs = {
799818
hash: Scalars['String']['input'];
800819
};
@@ -1000,6 +1019,19 @@ export type QueryTransfersArgs = {
10001019
requestKey?: InputMaybe<Scalars['String']['input']>;
10011020
};
10021021

1022+
/** Connection type for balance query results. */
1023+
export type QueryBalanceConnection = {
1024+
__typename?: 'QueryBalanceConnection';
1025+
edges: Array<QueryBalanceConnectionEdge>;
1026+
pageInfo: PageInfo;
1027+
};
1028+
1029+
export type QueryBalanceConnectionEdge = {
1030+
__typename?: 'QueryBalanceConnectionEdge';
1031+
cursor: Scalars['String']['output'];
1032+
node: BalanceNode;
1033+
};
1034+
10031035
export type QueryBlocksFromDepthConnection = {
10041036
__typename?: 'QueryBlocksFromDepthConnection';
10051037
edges: Array<QueryBlocksFromDepthConnectionEdge>;
@@ -1531,6 +1563,7 @@ export type ResolversInterfaceTypes<_RefType extends Record<string, unknown>> =
15311563

15321564
/** Mapping between all available schema types and the resolvers types */
15331565
export type ResolversTypes = {
1566+
BalanceNode: ResolverTypeWrapper<BalanceNode>;
15341567
BigInt: ResolverTypeWrapper<Scalars['BigInt']['output']>;
15351568
Block: ResolverTypeWrapper<
15361569
Omit<Block, 'events' | 'minerAccount' | 'parent' | 'transactions'> & {
@@ -1708,6 +1741,8 @@ export type ResolversTypes = {
17081741
PoolTransactionType: PoolTransactionType;
17091742
PoolTransactionsConnection: ResolverTypeWrapper<PoolTransactionsConnection>;
17101743
Query: ResolverTypeWrapper<{}>;
1744+
QueryBalanceConnection: ResolverTypeWrapper<QueryBalanceConnection>;
1745+
QueryBalanceConnectionEdge: ResolverTypeWrapper<QueryBalanceConnectionEdge>;
17111746
QueryBlocksFromDepthConnection: ResolverTypeWrapper<
17121747
Omit<QueryBlocksFromDepthConnection, 'edges'> & {
17131748
edges: Array<ResolversTypes['QueryBlocksFromDepthConnectionEdge']>;
@@ -1833,6 +1868,7 @@ export type ResolversTypes = {
18331868

18341869
/** Mapping between all available schema types and the resolvers parents */
18351870
export type ResolversParentTypes = {
1871+
BalanceNode: BalanceNode;
18361872
BigInt: Scalars['BigInt']['output'];
18371873
Block: Omit<Block, 'events' | 'minerAccount' | 'parent' | 'transactions'> & {
18381874
events: ResolversParentTypes['BlockEventsConnection'];
@@ -1980,6 +2016,8 @@ export type ResolversParentTypes = {
19802016
PoolTransactionEdge: PoolTransactionEdge;
19812017
PoolTransactionsConnection: PoolTransactionsConnection;
19822018
Query: {};
2019+
QueryBalanceConnection: QueryBalanceConnection;
2020+
QueryBalanceConnectionEdge: QueryBalanceConnectionEdge;
19832021
QueryBlocksFromDepthConnection: Omit<QueryBlocksFromDepthConnection, 'edges'> & {
19842022
edges: Array<ResolversParentTypes['QueryBlocksFromDepthConnectionEdge']>;
19852023
};
@@ -2087,6 +2125,16 @@ export type ComplexityDirectiveResolver<
20872125
Args = ComplexityDirectiveArgs,
20882126
> = DirectiveResolverFn<Result, Parent, ContextType, Args>;
20892127

2128+
export type BalanceNodeResolvers<
2129+
ContextType = any,
2130+
ParentType extends ResolversParentTypes['BalanceNode'] = ResolversParentTypes['BalanceNode'],
2131+
> = {
2132+
balance?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2133+
chainId?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2134+
module?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2135+
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
2136+
};
2137+
20902138
export interface BigIntScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['BigInt'], any> {
20912139
name: 'BigInt';
20922140
}
@@ -2878,6 +2926,12 @@ export type QueryResolvers<
28782926
ContextType = any,
28792927
ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query'],
28802928
> = {
2929+
balance?: Resolver<
2930+
ResolversTypes['QueryBalanceConnection'],
2931+
ParentType,
2932+
ContextType,
2933+
RequireFields<QueryBalanceArgs, 'accountName'>
2934+
>;
28812935
block?: Resolver<
28822936
Maybe<ResolversTypes['Block']>,
28832937
ParentType,
@@ -3054,6 +3108,26 @@ export type QueryResolvers<
30543108
>;
30553109
};
30563110

3111+
export type QueryBalanceConnectionResolvers<
3112+
ContextType = any,
3113+
ParentType extends
3114+
ResolversParentTypes['QueryBalanceConnection'] = ResolversParentTypes['QueryBalanceConnection'],
3115+
> = {
3116+
edges?: Resolver<Array<ResolversTypes['QueryBalanceConnectionEdge']>, ParentType, ContextType>;
3117+
pageInfo?: Resolver<ResolversTypes['PageInfo'], ParentType, ContextType>;
3118+
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
3119+
};
3120+
3121+
export type QueryBalanceConnectionEdgeResolvers<
3122+
ContextType = any,
3123+
ParentType extends
3124+
ResolversParentTypes['QueryBalanceConnectionEdge'] = ResolversParentTypes['QueryBalanceConnectionEdge'],
3125+
> = {
3126+
cursor?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
3127+
node?: Resolver<ResolversTypes['BalanceNode'], ParentType, ContextType>;
3128+
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
3129+
};
3130+
30573131
export type QueryBlocksFromDepthConnectionResolvers<
30583132
ContextType = any,
30593133
ParentType extends
@@ -3556,6 +3630,7 @@ export type UserGuardResolvers<
35563630
};
35573631

35583632
export type Resolvers<ContextType = any> = {
3633+
BalanceNode?: BalanceNodeResolvers<ContextType>;
35593634
BigInt?: GraphQLScalarType;
35603635
Block?: BlockResolvers<ContextType>;
35613636
BlockEventsConnection?: BlockEventsConnectionResolvers<ContextType>;
@@ -3612,6 +3687,8 @@ export type Resolvers<ContextType = any> = {
36123687
PoolTransactionEdge?: PoolTransactionEdgeResolvers<ContextType>;
36133688
PoolTransactionsConnection?: PoolTransactionsConnectionResolvers<ContextType>;
36143689
Query?: QueryResolvers<ContextType>;
3690+
QueryBalanceConnection?: QueryBalanceConnectionResolvers<ContextType>;
3691+
QueryBalanceConnectionEdge?: QueryBalanceConnectionEdgeResolvers<ContextType>;
36153692
QueryBlocksFromDepthConnection?: QueryBlocksFromDepthConnectionResolvers<ContextType>;
36163693
QueryBlocksFromDepthConnectionEdge?: QueryBlocksFromDepthConnectionEdgeResolvers<ContextType>;
36173694
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';
@@ -128,6 +129,7 @@ export const resolvers: Resolvers<ResolverContext> = {
128129
events: eventsSubscriptionResolver,
129130
},
130131
Query: {
132+
balance: balanceQueryResolver,
131133
block: blockQueryResolver,
132134
blocksFromDepth: blocksFromDepthQueryResolver,
133135
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)