Skip to content

Commit 902b3f1

Browse files
authored
Merge pull request #368 from hack-a-chain-software/transactions-pagination-fix
fix: transactions cursor pagination
2 parents 26fac21 + a26a9f5 commit 902b3f1

File tree

9 files changed

+112
-96
lines changed

9 files changed

+112
-96
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use strict';
2+
3+
/** @type {import('sequelize-cli').Migration} */
4+
module.exports = {
5+
async up(queryInterface) {
6+
await queryInterface.addIndex('Transactions', ['creationtime', 'id'], {
7+
name: 'transactions_creationtime_id_idx',
8+
});
9+
await queryInterface.removeIndex('Transactions', 'transactions_creationtime_idx');
10+
},
11+
12+
async down(queryInterface) {
13+
await queryInterface.addIndex('Transactions', {
14+
fields: ['creationtime'],
15+
name: 'transactions_creationtime_idx',
16+
});
17+
await queryInterface.removeIndex('Transactions', 'transactions_creationtime_id_idx');
18+
},
19+
};

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,7 @@ export type Query = {
730730
transaction?: Maybe<Transaction>;
731731
/**
732732
* Retrieve transactions. Default page size is 20.
733-
* At least one of accountName, fungibleName, blockHash, or requestKey must be provided.
733+
* At least one of accountName, fungibleName, blockHash, or requestKey must be provided.
734734
*/
735735
transactions: QueryTransactionsConnection;
736736
/** Retrieve all transactions by a given public key. */
@@ -914,6 +914,7 @@ export type QueryTransactionsArgs = {
914914
chainId?: InputMaybe<Scalars['String']['input']>;
915915
first?: InputMaybe<Scalars['Int']['input']>;
916916
fungibleName?: InputMaybe<Scalars['String']['input']>;
917+
isCoinbase?: InputMaybe<Scalars['Boolean']['input']>;
917918
last?: InputMaybe<Scalars['Int']['input']>;
918919
maxHeight?: InputMaybe<Scalars['Int']['input']>;
919920
minHeight?: InputMaybe<Scalars['Int']['input']>;

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ type Query {
294294

295295
"""
296296
Retrieve transactions. Default page size is 20.
297-
At least one of accountName, fungibleName, blockHash, or requestKey must be provided.
297+
At least one of accountName, fungibleName, blockHash, or requestKey must be provided.
298298
"""
299299
transactions(
300300
accountName: String
@@ -309,6 +309,7 @@ type Query {
309309
minHeight: Int
310310
minimumDepth: Int
311311
requestKey: String
312+
isCoinbase: Boolean
312313
): QueryTransactionsConnection! @complexity(value: 10, multipliers: ["first", "last"])
313314

314315
"""

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ export interface GetTransactionsCountParams {
7171
minHeight?: number | null;
7272
/** Filter by whether the transaction has a token ID (NFT transactions) */
7373
hasTokenId?: boolean | null;
74+
/** Filter by coinbase */
75+
isCoinbase?: boolean | null;
7476
}
7577

7678
/**

indexer/src/kadena-server/repository/infra/query-builders/transaction-query-builder.ts

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
* This class encapsulates the complex logic for constructing SQL queries
55
* to retrieve transactions from the database with various filtering criteria.
66
*/
7-
import { GetTransactionsParams } from '../../../repository/application/transaction-repository';
7+
import {
8+
GetTransactionsCountParams,
9+
GetTransactionsParams,
10+
} from '../../../repository/application/transaction-repository';
811

912
export default class TransactionQueryBuilder {
1013
/**
@@ -94,16 +97,20 @@ export default class TransactionQueryBuilder {
9497

9598
// Add 'after' cursor condition for pagination
9699
if (after) {
97-
transactionParams.push(after);
100+
const [creationTime, id] = after.split(':');
101+
transactionParams.push(creationTime);
98102
const op = localOperator(transactionParams.length);
99-
conditions += `${op} t.creationtime < $${queryParams.length + transactionParams.length}`;
103+
transactionParams.push(id);
104+
conditions += `${op} (t.creationtime, t.id) < ($${queryParams.length + transactionParams.length - 1}, $${queryParams.length + transactionParams.length})`;
100105
}
101106

102107
// Add 'before' cursor condition for pagination
103108
if (before) {
104-
transactionParams.push(before);
109+
const [creationTime, id] = before.split(':');
110+
transactionParams.push(creationTime);
105111
const op = localOperator(transactionParams.length);
106-
conditions += `${op} t.creationtime > $${queryParams.length + transactionParams.length}`;
112+
transactionParams.push(id);
113+
conditions += `${op} (t.creationtime, t.id) > ($${queryParams.length + transactionParams.length - 1}, $${queryParams.length + transactionParams.length})`;
107114
}
108115

109116
// Add request key condition for exact transaction lookup
@@ -151,7 +158,7 @@ export default class TransactionQueryBuilder {
151158
* @returns Object containing the query string and parameters array
152159
*/
153160
buildTransactionsQuery(
154-
params: GetTransactionsParams & {
161+
params: GetTransactionsCountParams & {
155162
after?: string | null;
156163
before?: string | null;
157164
order: string;
@@ -234,9 +241,9 @@ export default class TransactionQueryBuilder {
234241
t.requestkey AS "requestKey"
235242
FROM filtered_block b
236243
JOIN "Transactions" t ON b.id = t."blockId"
237-
LEFT JOIN "TransactionDetails" td ON t.id = td."transactionId"
244+
${params.isCoinbase ? 'LEFT ' : ''} JOIN "TransactionDetails" td ON t.id = td."transactionId"
238245
${transactionsConditions}
239-
ORDER BY t.creationtime ${order}
246+
ORDER BY t.creationtime ${order}, t.id ${order}
240247
LIMIT $1
241248
`;
242249
} else {
@@ -246,7 +253,7 @@ export default class TransactionQueryBuilder {
246253
SELECT t.id, t."blockId", t.hash, t.num_events, t.txid, t.logs, t.result, t.requestkey, t."chainId", t.creationtime
247254
FROM "Transactions" t
248255
${transactionsConditions}
249-
ORDER BY t.creationtime ${order}
256+
ORDER BY t.creationtime ${order}, t.id ${order}
250257
LIMIT $1
251258
)
252259
SELECT
@@ -275,7 +282,7 @@ export default class TransactionQueryBuilder {
275282
t.requestkey AS "requestKey"
276283
FROM filtered_transactions t
277284
JOIN "Blocks" b ON b.id = t."blockId"
278-
LEFT JOIN "TransactionDetails" td ON t.id = td."transactionId"
285+
${params.isCoinbase ? 'LEFT ' : ''} JOIN "TransactionDetails" td ON t.id = td."transactionId"
279286
${blocksConditions}
280287
`;
281288
}
@@ -288,22 +295,25 @@ export default class TransactionQueryBuilder {
288295
before?: string | null;
289296
order: string;
290297
limit: number;
298+
isCoinbase: boolean;
291299
}) {
292300
let whereCondition = '';
293301
let queryParams: (string | number)[] = [params.limit];
294302

295303
if (!params.after && !params.before) {
296-
const currentTime = Date.now() - 100000;
297-
queryParams.push(currentTime);
298-
whereCondition = ` WHERE t.creationtime > $2`;
304+
const currentTime = Date.now() - 10000000;
305+
queryParams.push(currentTime, 0);
306+
whereCondition = ` WHERE t.creationtime > $2 AND t.id > $3`;
299307
}
300308
if (params.after) {
301-
queryParams.push(params.after);
302-
whereCondition = ` WHERE t.creationtime < $2`;
309+
const [creationTime, id] = params.after.split(':');
310+
queryParams.push(creationTime, id);
311+
whereCondition = ` WHERE (t.creationtime, t.id) < ($2, $3)`;
303312
}
304313
if (params.before) {
305-
queryParams.push(params.before);
306-
whereCondition = ` WHERE t.creationtime > $2`;
314+
const [creationTime, id] = params.before.split(':');
315+
queryParams.push(creationTime, id);
316+
whereCondition = ` WHERE (t.creationtime, t.id) > ($2, $3)`;
307317
}
308318

309319
let query = `
@@ -331,12 +341,11 @@ export default class TransactionQueryBuilder {
331341
t.requestkey AS "requestKey"
332342
FROM "Transactions" t
333343
JOIN "Blocks" b ON b.id = t."blockId"
334-
LEFT JOIN "TransactionDetails" td ON t.id = td."transactionId"
344+
${params.isCoinbase ? 'LEFT ' : ''} JOIN "TransactionDetails" td ON t.id = td."transactionId"
335345
${whereCondition}
336-
ORDER BY t.creationtime ${params.order}
346+
ORDER BY t.creationtime ${params.order}, t.id ${params.order}
337347
LIMIT $1
338348
`;
339-
340349
return { query, queryParams };
341350
}
342351
}

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

Lines changed: 22 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -69,27 +69,17 @@ export default class TransactionDbRepository implements TransactionRepository {
6969
order,
7070
after,
7171
before,
72+
isCoinbase: !!rest.isCoinbase,
7273
});
7374

7475
// Execute the query with the constructed parameters
7576
const { rows } = await rootPgPool.query(query, queryParams);
7677

7778
// Transform database rows into GraphQL-compatible edges with cursors
78-
const edges = rows
79-
.map(row => ({
80-
cursor: row.creationTime.toString(),
81-
node: transactionValidator.validate(row),
82-
}))
83-
.sort((a, b) => {
84-
// Primary sort is already done by DB query (creationTime DESC)
85-
// Add secondary sort by id for consistent ordering when creationTimes are equal
86-
const aNode = a.node as unknown as { id: string };
87-
const bNode = b.node as unknown as { id: string };
88-
if (a.cursor === b.cursor) {
89-
return aNode.id > bNode.id ? 1 : -1;
90-
}
91-
return 0; // Maintain existing order from DB for different creationTimes
92-
});
79+
const edges = rows.map(row => ({
80+
cursor: `${row.creationTime.toString()}:${row.id.toString()}`,
81+
node: transactionValidator.validate(row),
82+
}));
9383

9484
const pageInfo = getPageInfo({ edges, order, limit, after, before });
9585
return pageInfo;
@@ -110,21 +100,10 @@ export default class TransactionDbRepository implements TransactionRepository {
110100
const { rows } = await rootPgPool.query(query, queryParams);
111101

112102
// Transform database rows into GraphQL-compatible edges with cursors
113-
const edges = rows
114-
.map(row => ({
115-
cursor: row.creationTime.toString(),
116-
node: transactionValidator.validate(row),
117-
}))
118-
.sort((a, b) => {
119-
// Primary sort is already done by DB query (creationTime DESC)
120-
// Add secondary sort by id for consistent ordering when creationTimes are equal
121-
const aNode = a.node as unknown as { id: string };
122-
const bNode = b.node as unknown as { id: string };
123-
if (a.cursor === b.cursor) {
124-
return aNode.id > bNode.id ? 1 : -1;
125-
}
126-
return 0; // Maintain existing order from DB for different creationTimes
127-
});
103+
const edges = rows.map(row => ({
104+
cursor: `${row.creationTime.toString()}:${row.id.toString()}`,
105+
node: transactionValidator.validate(row),
106+
}));
128107

129108
const pageInfo = getPageInfo({ edges, order, limit, after, before });
130109
return pageInfo;
@@ -174,26 +153,15 @@ export default class TransactionDbRepository implements TransactionRepository {
174153
// Update cursor for next batch
175154
if (transactionBatch.length > 0) {
176155
const lastTransaction = transactionBatch[transactionBatch.length - 1];
177-
lastCursor = lastTransaction.creationTime.toString();
156+
lastCursor = `${lastTransaction.creationTime.toString()}:${lastTransaction.id.toString()}`;
178157
}
179158
}
180159

181160
// Create edges for paginated result and apply sorting
182-
const edges = allFilteredTransactions
183-
.slice(0, limit)
184-
.map(tx => ({
185-
cursor: tx.creationTime.toString(),
186-
node: transactionValidator.validate(tx),
187-
}))
188-
.sort((a, b) => {
189-
// Apply same sorting as in non-minimumDepth case
190-
const aNode = a.node as unknown as { id: string };
191-
const bNode = b.node as unknown as { id: string };
192-
if (a.cursor === b.cursor) {
193-
return aNode.id > bNode.id ? 1 : -1;
194-
}
195-
return 0;
196-
});
161+
const edges = allFilteredTransactions.slice(0, limit).map(tx => ({
162+
cursor: `${tx.creationTime.toString()}:${tx.id.toString()}`,
163+
node: transactionValidator.validate(tx),
164+
}));
197165

198166
return getPageInfo({ edges, order, limit, after, before });
199167
}
@@ -402,13 +370,15 @@ export default class TransactionDbRepository implements TransactionRepository {
402370
let cursorCondition = '';
403371

404372
if (after) {
405-
cursorCondition = `\nWHERE t.creationtime < $3`;
406-
queryParams.push(after);
373+
const [creationTime, id] = after.split(':');
374+
cursorCondition = `\nWHERE (t.creationtime, t.id) < ($3, $4)`;
375+
queryParams.push(creationTime, id);
407376
}
408377

409378
if (before) {
410-
cursorCondition = `\nWHERE t.creationtime > $3`;
411-
queryParams.push(before);
379+
const [creationTime, id] = before.split(':');
380+
cursorCondition = `\nWHERE (t.creationtime, t.id) > ($3, $4)`;
381+
queryParams.push(creationTime, id);
412382
}
413383

414384
const query = `
@@ -443,14 +413,14 @@ export default class TransactionDbRepository implements TransactionRepository {
443413
) filtered_signers ON t.id = filtered_signers."transactionId"
444414
LEFT JOIN "TransactionDetails" td on t.id = td."transactionId"
445415
${cursorCondition}
446-
ORDER BY t.creationtime ${order}
416+
ORDER BY t.creationtime ${order}, t.id ${order}
447417
LIMIT $1;
448418
`;
449419

450420
const { rows } = await rootPgPool.query(query, queryParams);
451421

452422
const edges = rows.map(row => ({
453-
cursor: row.creationTime.toString(),
423+
cursor: `${row.creationTime.toString()}:${row.id.toString()}`,
454424
node: transactionValidator.validate(row),
455425
}));
456426

indexer/src/kadena-server/resolvers/query/transactions-query-resolver.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export const transactionsQueryResolver: QueryResolvers<ResolverContext>['transac
5656
maxHeight,
5757
minHeight,
5858
minimumDepth,
59+
isCoinbase = false,
5960
} = args;
6061

6162
// Call the repository layer to retrieve the filtered and paginated transactions
@@ -69,6 +70,7 @@ export const transactionsQueryResolver: QueryResolvers<ResolverContext>['transac
6970
maxHeight,
7071
minHeight,
7172
minimumDepth,
73+
isCoinbase,
7274
first,
7375
last,
7476
before,
@@ -100,6 +102,7 @@ export const transactionsQueryResolver: QueryResolvers<ResolverContext>['transac
100102
maxHeight,
101103
minHeight,
102104
minimumDepth,
105+
isCoinbase,
103106
fungibleName,
104107
requestKey,
105108
};

0 commit comments

Comments
 (0)