@o1-labs/mina-archive-node-graphql 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +321 -0
  2. package/build/benchmark/setup.js +32 -0
  3. package/build/benchmark/setup.js.map +1 -0
  4. package/build/src/blockchain/constants.js +2 -0
  5. package/build/src/blockchain/constants.js.map +1 -0
  6. package/build/src/blockchain/types.js +7 -0
  7. package/build/src/blockchain/types.js.map +1 -0
  8. package/build/src/blockchain/utils.js +39 -0
  9. package/build/src/blockchain/utils.js.map +1 -0
  10. package/build/src/consensus/mina-consensus.js +59 -0
  11. package/build/src/consensus/mina-consensus.js.map +1 -0
  12. package/build/src/context.js +10 -0
  13. package/build/src/context.js.map +1 -0
  14. package/build/src/db/archive-node-adapter/archive-node-adapter.interface.js +2 -0
  15. package/build/src/db/archive-node-adapter/archive-node-adapter.interface.js.map +1 -0
  16. package/build/src/db/archive-node-adapter/archive-node-adapter.js +47 -0
  17. package/build/src/db/archive-node-adapter/archive-node-adapter.js.map +1 -0
  18. package/build/src/db/sql/events-actions/queries.js +438 -0
  19. package/build/src/db/sql/events-actions/queries.js.map +1 -0
  20. package/build/src/db/sql/events-actions/types.js +2 -0
  21. package/build/src/db/sql/events-actions/types.js.map +1 -0
  22. package/build/src/errors/error.js +17 -0
  23. package/build/src/errors/error.js.map +1 -0
  24. package/build/src/index.js +27 -0
  25. package/build/src/index.js.map +1 -0
  26. package/build/src/resolvers-types.js +8 -0
  27. package/build/src/resolvers-types.js.map +1 -0
  28. package/build/src/resolvers.js +67 -0
  29. package/build/src/resolvers.js.map +1 -0
  30. package/build/src/server/plugins.js +39 -0
  31. package/build/src/server/plugins.js.map +1 -0
  32. package/build/src/server/server.js +25 -0
  33. package/build/src/server/server.js.map +1 -0
  34. package/build/src/services/actions-service/actions-service.interface.js +2 -0
  35. package/build/src/services/actions-service/actions-service.interface.js.map +1 -0
  36. package/build/src/services/actions-service/actions-service.js +94 -0
  37. package/build/src/services/actions-service/actions-service.js.map +1 -0
  38. package/build/src/services/blocks-service/blocks-service.interface.js +2 -0
  39. package/build/src/services/blocks-service/blocks-service.interface.js.map +1 -0
  40. package/build/src/services/blocks-service/blocks-service.js +263 -0
  41. package/build/src/services/blocks-service/blocks-service.js.map +1 -0
  42. package/build/src/services/data-adapters/database-row-adapters.js +116 -0
  43. package/build/src/services/data-adapters/database-row-adapters.js.map +1 -0
  44. package/build/src/services/events-service/events-service.interface.js +2 -0
  45. package/build/src/services/events-service/events-service.interface.js.map +1 -0
  46. package/build/src/services/events-service/events-service.js +64 -0
  47. package/build/src/services/events-service/events-service.js.map +1 -0
  48. package/build/src/services/network-service/network-service.interface.js +2 -0
  49. package/build/src/services/network-service/network-service.interface.js.map +1 -0
  50. package/build/src/services/network-service/network-service.js +31 -0
  51. package/build/src/services/network-service/network-service.js.map +1 -0
  52. package/build/src/tracing/jaeger-setup.js +39 -0
  53. package/build/src/tracing/jaeger-setup.js.map +1 -0
  54. package/build/src/tracing/jaeger-tracing.js +29 -0
  55. package/build/src/tracing/jaeger-tracing.js.map +1 -0
  56. package/build/src/tracing/tracer.js +31 -0
  57. package/build/src/tracing/tracer.js.map +1 -0
  58. package/build/tests/__mocks__/generate_mock_data.js +13 -0
  59. package/build/tests/__mocks__/generate_mock_data.js.map +1 -0
  60. package/build/tests/consensus/run-compare.js +40 -0
  61. package/build/tests/consensus/run-compare.js.map +1 -0
  62. package/build/tests/consensus/select-consensus-precomputed.js +80 -0
  63. package/build/tests/consensus/select-consensus-precomputed.js.map +1 -0
  64. package/build/tests/consensus/types.js +15 -0
  65. package/build/tests/consensus/types.js.map +1 -0
  66. package/build/tests/devnet-dump/devnet-dump.test.js +311 -0
  67. package/build/tests/devnet-dump/devnet-dump.test.js.map +1 -0
  68. package/build/tests/devnet-dump/setup.js +58 -0
  69. package/build/tests/devnet-dump/setup.js.map +1 -0
  70. package/build/tests/integration/integration.test.js +226 -0
  71. package/build/tests/integration/integration.test.js.map +1 -0
  72. package/build/tests/integration/setup.js +82 -0
  73. package/build/tests/integration/setup.js.map +1 -0
  74. package/build/tests/live-api/actions.test.js +4 -0
  75. package/build/tests/live-api/actions.test.js.map +1 -0
  76. package/build/tests/live-api/blocks.test.js +116 -0
  77. package/build/tests/live-api/blocks.test.js.map +1 -0
  78. package/build/tests/live-api/events.test.js +97 -0
  79. package/build/tests/live-api/events.test.js.map +1 -0
  80. package/build/tests/live-api/networkState.test.js +27 -0
  81. package/build/tests/live-api/networkState.test.js.map +1 -0
  82. package/build/tests/live-network/live-network.test.js +144 -0
  83. package/build/tests/live-network/live-network.test.js.map +1 -0
  84. package/build/tests/live-network/setup.js +206 -0
  85. package/build/tests/live-network/setup.js.map +1 -0
  86. package/build/tests/resolvers.test.js +475 -0
  87. package/build/tests/resolvers.test.js.map +1 -0
  88. package/build/tests/services/actions-service/actions-service.test.js +124 -0
  89. package/build/tests/services/actions-service/actions-service.test.js.map +1 -0
  90. package/build/tests/services/blocks-service/blocks-service.test.js +348 -0
  91. package/build/tests/services/blocks-service/blocks-service.test.js.map +1 -0
  92. package/build/tests/services/events-service/events-service.test.js +185 -0
  93. package/build/tests/services/events-service/events-service.test.js.map +1 -0
  94. package/build/tests/test-helpers.js +11 -0
  95. package/build/tests/test-helpers.js.map +1 -0
  96. package/build/tests/unit/blockchain-utils.test.js +106 -0
  97. package/build/tests/unit/blockchain-utils.test.js.map +1 -0
  98. package/build/tests/unit/consensus.test.js +154 -0
  99. package/build/tests/unit/consensus.test.js.map +1 -0
  100. package/build/tests/unit/error-handling.test.js +52 -0
  101. package/build/tests/unit/error-handling.test.js.map +1 -0
  102. package/build/tests/utils.test.js +313 -0
  103. package/build/tests/utils.test.js.map +1 -0
  104. package/build/zkapp/contract.js +144 -0
  105. package/build/zkapp/contract.js.map +1 -0
  106. package/build/zkapp/utils.js +174 -0
  107. package/build/zkapp/utils.js.map +1 -0
  108. package/package.json +85 -0
  109. package/schema.graphql +221 -0
  110. package/src/blockchain/constants.ts +2 -0
  111. package/src/blockchain/types.ts +121 -0
  112. package/src/blockchain/utils.ts +54 -0
  113. package/src/consensus/mina-consensus.ts +137 -0
  114. package/src/context.ts +18 -0
  115. package/src/db/archive-node-adapter/archive-node-adapter.interface.ts +23 -0
  116. package/src/db/archive-node-adapter/archive-node-adapter.ts +101 -0
  117. package/src/db/sql/events-actions/queries.ts +490 -0
  118. package/src/db/sql/events-actions/types.ts +208 -0
  119. package/src/envionment.d.ts +18 -0
  120. package/src/errors/error.ts +20 -0
  121. package/src/index.ts +31 -0
  122. package/src/resolvers-types.ts +754 -0
  123. package/src/resolvers.ts +102 -0
  124. package/src/server/plugins.ts +56 -0
  125. package/src/server/server.ts +30 -0
  126. package/src/services/actions-service/actions-service.interface.ts +9 -0
  127. package/src/services/actions-service/actions-service.ts +195 -0
  128. package/src/services/blocks-service/blocks-service.interface.ts +11 -0
  129. package/src/services/blocks-service/blocks-service.ts +407 -0
  130. package/src/services/data-adapters/database-row-adapters.ts +261 -0
  131. package/src/services/events-service/events-service.interface.ts +6 -0
  132. package/src/services/events-service/events-service.ts +118 -0
  133. package/src/services/network-service/network-service.interface.ts +5 -0
  134. package/src/services/network-service/network-service.ts +53 -0
  135. package/src/tracing/jaeger-setup.ts +65 -0
  136. package/src/tracing/jaeger-tracing.ts +44 -0
  137. package/src/tracing/tracer.ts +52 -0
  138. package/tsconfig.json +104 -0
@@ -0,0 +1,407 @@
1
+ import type postgres from 'postgres';
2
+ import {
3
+ Blocks,
4
+ UserCommand,
5
+ ZkAppCommand,
6
+ FeeTransfer,
7
+ } from '../../blockchain/types.js';
8
+ import type {
9
+ BlockQueryInput,
10
+ BlockSortByInput,
11
+ } from '../../resolvers-types.js';
12
+ import { IBlocksService } from './blocks-service.interface.js';
13
+ import {
14
+ TracingState,
15
+ extractTraceStateFromOptions,
16
+ } from '../../tracing/tracer.js';
17
+ import {
18
+ BLOCK_RANGE_SIZE,
19
+ ENABLE_BLOCK_TRANSACTION_DETAILS,
20
+ } from '../../server/server.js';
21
+
22
+ export { BlocksService };
23
+ export type { BlockRow, UserCommandRow, ZkAppCommandRow, FeeTransferRow };
24
+
25
+ // Interface representing the expected row structure from the database
26
+ interface BlockRow {
27
+ id: number;
28
+ height: number;
29
+ creator: string;
30
+ state_hash: string;
31
+ parent_hash: string;
32
+ timestamp: string;
33
+ internal_command_type?: string;
34
+ coinbase_amount?: string;
35
+ }
36
+
37
+ interface UserCommandRow {
38
+ block_id: number;
39
+ hash: string;
40
+ kind: string;
41
+ from: string;
42
+ to: string;
43
+ amount: string;
44
+ fee: string;
45
+ memo: string;
46
+ nonce: number;
47
+ status: string;
48
+ failure_reason: string | null;
49
+ }
50
+
51
+ interface ZkAppCommandRow {
52
+ block_id: number;
53
+ hash: string;
54
+ fee_payer: string;
55
+ fee: string;
56
+ memo: string;
57
+ status: string;
58
+ failure_reasons_ids: number[] | string | null;
59
+ }
60
+
61
+ interface FeeTransferRow {
62
+ block_id: number;
63
+ recipient: string;
64
+ fee: string;
65
+ type: string;
66
+ }
67
+
68
+ class BlocksService implements IBlocksService {
69
+ private readonly client: postgres.Sql;
70
+
71
+ constructor(client: postgres.Sql) {
72
+ this.client = client;
73
+ }
74
+
75
+ async getBlocks(
76
+ query: BlockQueryInput | null | undefined,
77
+ limit: number | null | undefined,
78
+ sortBy: BlockSortByInput | null | undefined,
79
+ options: unknown
80
+ ): Promise<Blocks> {
81
+ const tracingState = extractTraceStateFromOptions(options);
82
+ return (await this.getBlockData(query, limit, sortBy, { tracingState })) ?? [];
83
+ }
84
+
85
+ async getBlockData(
86
+ query: BlockQueryInput | null | undefined,
87
+ limit: number | null | undefined,
88
+ sortBy: BlockSortByInput | null | undefined,
89
+ { tracingState }: { tracingState: TracingState }
90
+ ): Promise<Blocks> {
91
+ const sqlSpan = tracingState.startSpan('blocks.SQL');
92
+ const rows = await this.executeBlocksQuery(query, limit, sortBy);
93
+ sqlSpan.end();
94
+
95
+ const blockRows = rows as unknown as BlockRow[];
96
+
97
+ let userCommandsByBlock = new Map<number, UserCommand[]>();
98
+ let zkappCommandsByBlock = new Map<number, ZkAppCommand[]>();
99
+ let feeTransfersByBlock = new Map<number, FeeTransfer[]>();
100
+
101
+ if (ENABLE_BLOCK_TRANSACTION_DETAILS && blockRows.length > 0) {
102
+ const txSpan = tracingState.startSpan('blocks.transactionDetails.SQL');
103
+ const blockIds = blockRows.map((r) => r.id);
104
+ [userCommandsByBlock, zkappCommandsByBlock, feeTransfersByBlock] =
105
+ await this.fetchTransactionDetails(blockIds);
106
+ txSpan.end();
107
+ }
108
+
109
+ const processingSpan = tracingState.startSpan('blocks.processing');
110
+ const blocks = this.rowsToBlocks(
111
+ blockRows,
112
+ userCommandsByBlock,
113
+ zkappCommandsByBlock,
114
+ feeTransfersByBlock
115
+ );
116
+ processingSpan.end();
117
+ return blocks;
118
+ }
119
+
120
+ private async executeBlocksQuery(
121
+ query: BlockQueryInput | null | undefined,
122
+ limit: number | null | undefined,
123
+ sortBy: BlockSortByInput | null | undefined
124
+ ) {
125
+ const blockHeightGte = query?.blockHeight_gte;
126
+ const blockHeightLt = query?.blockHeight_lt;
127
+ const dateTimeGte = query?.dateTime_gte;
128
+ const dateTimeLt = query?.dateTime_lt;
129
+ const canonical = query?.canonical;
130
+ const inBestChain = query?.inBestChain;
131
+ const orderBy = sortBy === 'BLOCKHEIGHT_DESC' ? 'DESC' : 'ASC';
132
+ const limitValue = Math.min(limit ?? 200, BLOCK_RANGE_SIZE);
133
+
134
+ // Build the SQL query for blocks with transactions
135
+ // Archive Node uses a junction table blocks_internal_commands
136
+ let sql = `
137
+ SELECT
138
+ b.id,
139
+ b.state_hash,
140
+ b.parent_hash,
141
+ b.height,
142
+ b.timestamp,
143
+ pk.value as creator,
144
+ COALESCE(ic.fee, '0') as coinbase_amount
145
+ FROM blocks b
146
+ INNER JOIN public_keys pk ON b.creator_id = pk.id
147
+ LEFT JOIN (
148
+ SELECT
149
+ bic.block_id,
150
+ ic.fee
151
+ FROM internal_commands ic
152
+ INNER JOIN blocks_internal_commands bic ON ic.id = bic.internal_command_id
153
+ WHERE ic.command_type = 'coinbase'
154
+ ) AS ic ON b.id = ic.block_id
155
+ WHERE TRUE
156
+ `;
157
+
158
+ const params: (string | number)[] = [];
159
+ let paramIndex = 1;
160
+
161
+ if (canonical === true) {
162
+ sql += ` AND b.chain_status = 'canonical'`;
163
+ } else if (canonical === false) {
164
+ sql += ` AND b.chain_status <> 'canonical'`;
165
+ }
166
+
167
+ if (blockHeightGte) {
168
+ sql += ` AND b.height >= $${paramIndex}`;
169
+ params.push(blockHeightGte);
170
+ paramIndex++;
171
+ }
172
+
173
+ if (blockHeightLt) {
174
+ sql += ` AND b.height < $${paramIndex}`;
175
+ params.push(blockHeightLt);
176
+ paramIndex++;
177
+ }
178
+
179
+ if (dateTimeGte) {
180
+ // Convert ISO timestamp to Unix milliseconds
181
+ const timestampMs = new Date(dateTimeGte).getTime().toString();
182
+ sql += ` AND b.timestamp >= $${paramIndex}`;
183
+ params.push(timestampMs);
184
+ paramIndex++;
185
+ }
186
+
187
+ if (dateTimeLt) {
188
+ // Convert ISO timestamp to Unix milliseconds
189
+ const timestampMs = new Date(dateTimeLt).getTime().toString();
190
+ sql += ` AND b.timestamp < $${paramIndex}`;
191
+ params.push(timestampMs);
192
+ paramIndex++;
193
+ }
194
+
195
+ const best_chain_til_canonical_cte =
196
+ `
197
+ WITH RECURSIVE best_chain_til_canonical AS (
198
+ SELECT
199
+ id,
200
+ parent_id,
201
+ height
202
+ FROM blocks
203
+ WHERE height = (SELECT MAX(height) FROM blocks)
204
+ AND chain_status = 'pending'
205
+
206
+ UNION
207
+
208
+ SELECT
209
+ potential_parent.id,
210
+ potential_parent.parent_id,
211
+ potential_parent.height
212
+ FROM blocks potential_parent
213
+ JOIN best_chain_til_canonical ON potential_parent.id = best_chain_til_canonical.parent_id
214
+ WHERE potential_parent.chain_status <> 'canonical'
215
+ )
216
+ `
217
+ if (inBestChain === true) {
218
+ sql =
219
+ best_chain_til_canonical_cte
220
+ + sql
221
+ + ` AND (b.id IN (SELECT id FROM best_chain_til_canonical) OR b.chain_status = 'canonical')`;
222
+ } else if (inBestChain === false) {
223
+ sql =
224
+ best_chain_til_canonical_cte
225
+ + sql
226
+ + ` AND (b.id NOT IN (SELECT id FROM best_chain_til_canonical)
227
+ OR b.chain_status = 'orphaned')`;
228
+
229
+ }
230
+
231
+ sql += ` ORDER BY b.height ${orderBy}`;
232
+ sql += ` LIMIT $${paramIndex}`;
233
+ params.push(limitValue);
234
+
235
+ return this.client.unsafe(sql, params);
236
+ }
237
+
238
+ private async fetchTransactionDetails(
239
+ blockIds: number[]
240
+ ): Promise<
241
+ [
242
+ Map<number, UserCommand[]>,
243
+ Map<number, ZkAppCommand[]>,
244
+ Map<number, FeeTransfer[]>,
245
+ ]
246
+ > {
247
+ const [ucRows, zkRows, ftRows] = await Promise.all([
248
+ this.fetchUserCommands(blockIds),
249
+ this.fetchZkAppCommands(blockIds),
250
+ this.fetchFeeTransfers(blockIds),
251
+ ]);
252
+
253
+ return [
254
+ this.groupBy(
255
+ ucRows as unknown as UserCommandRow[],
256
+ (r) => r.block_id,
257
+ this.mapUserCommandRow
258
+ ),
259
+ this.groupBy(
260
+ zkRows as unknown as ZkAppCommandRow[],
261
+ (r) => r.block_id,
262
+ this.mapZkAppCommandRow
263
+ ),
264
+ this.groupBy(
265
+ ftRows as unknown as FeeTransferRow[],
266
+ (r) => r.block_id,
267
+ this.mapFeeTransferRow
268
+ ),
269
+ ];
270
+ }
271
+
272
+ private async fetchUserCommands(blockIds: number[]) {
273
+ const sql = `
274
+ SELECT
275
+ buc.block_id,
276
+ uc.hash,
277
+ uc.command_type AS kind,
278
+ pk_from.value AS "from",
279
+ pk_to.value AS "to",
280
+ COALESCE(uc.amount, '0') AS amount,
281
+ uc.fee,
282
+ uc.memo,
283
+ uc.nonce,
284
+ buc.status,
285
+ buc.failure_reason
286
+ FROM blocks_user_commands buc
287
+ INNER JOIN user_commands uc ON buc.user_command_id = uc.id
288
+ INNER JOIN public_keys pk_from ON uc.source_id = pk_from.id
289
+ INNER JOIN public_keys pk_to ON uc.receiver_id = pk_to.id
290
+ WHERE buc.block_id = ANY($1)
291
+ ORDER BY buc.block_id, buc.sequence_no
292
+ `;
293
+ return this.client.unsafe(sql, [blockIds]);
294
+ }
295
+
296
+ private async fetchZkAppCommands(blockIds: number[]) {
297
+ const sql = `
298
+ SELECT
299
+ bzkc.block_id,
300
+ zkc.hash,
301
+ pk_fee.value AS fee_payer,
302
+ fpb.fee,
303
+ zkc.memo,
304
+ bzkc.status,
305
+ bzkc.failure_reasons_ids
306
+ FROM blocks_zkapp_commands bzkc
307
+ INNER JOIN zkapp_commands zkc ON bzkc.zkapp_command_id = zkc.id
308
+ INNER JOIN zkapp_fee_payer_body fpb ON zkc.zkapp_fee_payer_body_id = fpb.id
309
+ INNER JOIN public_keys pk_fee ON fpb.public_key_id = pk_fee.id
310
+ WHERE bzkc.block_id = ANY($1)
311
+ ORDER BY bzkc.block_id, bzkc.sequence_no
312
+ `;
313
+ return this.client.unsafe(sql, [blockIds]);
314
+ }
315
+
316
+ private async fetchFeeTransfers(blockIds: number[]) {
317
+ const sql = `
318
+ SELECT
319
+ bic.block_id,
320
+ pk.value AS recipient,
321
+ ic.fee,
322
+ ic.command_type AS type
323
+ FROM blocks_internal_commands bic
324
+ INNER JOIN internal_commands ic ON bic.internal_command_id = ic.id
325
+ INNER JOIN public_keys pk ON ic.receiver_id = pk.id
326
+ WHERE bic.block_id = ANY($1)
327
+ AND ic.command_type IN ('fee_transfer', 'fee_transfer_via_coinbase')
328
+ ORDER BY bic.block_id, bic.sequence_no
329
+ `;
330
+ return this.client.unsafe(sql, [blockIds]);
331
+ }
332
+
333
+ groupBy<TRow, TResult>(
334
+ rows: TRow[],
335
+ keyFn: (row: TRow) => number,
336
+ mapFn: (row: TRow) => TResult
337
+ ): Map<number, TResult[]> {
338
+ const map = new Map<number, TResult[]>();
339
+ for (const row of rows) {
340
+ const key = keyFn(row);
341
+ let arr = map.get(key);
342
+ if (!arr) {
343
+ arr = [];
344
+ map.set(key, arr);
345
+ }
346
+ arr.push(mapFn(row));
347
+ }
348
+ return map;
349
+ }
350
+
351
+ mapUserCommandRow(row: UserCommandRow): UserCommand {
352
+ return {
353
+ hash: row.hash,
354
+ kind: row.kind,
355
+ from: row.from,
356
+ to: row.to,
357
+ amount: row.amount,
358
+ fee: row.fee,
359
+ memo: row.memo,
360
+ nonce: row.nonce,
361
+ status: row.status,
362
+ failureReason: row.failure_reason,
363
+ };
364
+ }
365
+
366
+ mapZkAppCommandRow(row: ZkAppCommandRow): ZkAppCommand {
367
+ return {
368
+ hash: row.hash,
369
+ feePayer: row.fee_payer,
370
+ fee: row.fee,
371
+ memo: row.memo,
372
+ status: row.status,
373
+ failureReason: row.failure_reasons_ids
374
+ ? String(row.failure_reasons_ids)
375
+ : null,
376
+ };
377
+ }
378
+
379
+ mapFeeTransferRow(row: FeeTransferRow): FeeTransfer {
380
+ return {
381
+ recipient: row.recipient,
382
+ fee: row.fee,
383
+ type: row.type,
384
+ };
385
+ }
386
+
387
+ rowsToBlocks(
388
+ rows: BlockRow[],
389
+ userCommandsByBlock: Map<number, UserCommand[]> = new Map(),
390
+ zkappCommandsByBlock: Map<number, ZkAppCommand[]> = new Map(),
391
+ feeTransfersByBlock: Map<number, FeeTransfer[]> = new Map()
392
+ ): Blocks {
393
+ return rows.map((row) => ({
394
+ blockHeight: row.height,
395
+ creator: row.creator,
396
+ stateHash: row.state_hash,
397
+ parentHash: ENABLE_BLOCK_TRANSACTION_DETAILS ? row.parent_hash : '',
398
+ dateTime: new Date(parseInt(row.timestamp)).toISOString(),
399
+ transactions: {
400
+ coinbase: row.coinbase_amount || '0',
401
+ userCommands: userCommandsByBlock.get(row.id) ?? [],
402
+ zkappCommands: zkappCommandsByBlock.get(row.id) ?? [],
403
+ feeTransfer: feeTransfersByBlock.get(row.id) ?? [],
404
+ },
405
+ }));
406
+ }
407
+ }
@@ -0,0 +1,261 @@
1
+ import type {
2
+ Action,
3
+ Event,
4
+ BlocksWithTransactionsMap,
5
+ FieldElementIdWithValueMap,
6
+ BlockInfo,
7
+ } from '../../blockchain/types.js';
8
+ import type { ArchiveNodeDatabaseRow } from '../../db/sql/events-actions/types.js';
9
+ import {
10
+ createTransactionInfo,
11
+ createEvent,
12
+ createAction,
13
+ } from '../../blockchain/utils.js';
14
+ import { filterBestTip } from '../../consensus/mina-consensus.js';
15
+
16
+ export {
17
+ partitionBlocks,
18
+ getElementIdFieldValues,
19
+ mapActionOrEvent,
20
+ removeRedundantEmittedFields,
21
+ sortAndFilterBlocks,
22
+ };
23
+
24
+ /**
25
+ * Partitions the given rows into a map where the keys are block hashes
26
+ * and the values are maps of transaction hashes to array of rows.
27
+ *
28
+ * For each row, if a block with the same block hash exists in the map,
29
+ * it adds the row to the corresponding transaction in the map.
30
+ * If the block does not exist, it creates a new map entry with the block hash.
31
+ *
32
+ * If the transaction does not exist within a block, it creates a new transaction entry with the transaction hash.
33
+ *
34
+ * @param rows The array of rows to be partitioned.
35
+ * @returns A map where the keys are block hashes and the values are maps of transaction hashes to array of rows.
36
+ */
37
+ function partitionBlocks(rows: ArchiveNodeDatabaseRow[]) {
38
+ const blocks: BlocksWithTransactionsMap = new Map();
39
+ if (rows.length === 0) return blocks;
40
+
41
+ for (let i = 0; i < rows.length; i++) {
42
+ const { state_hash: blockHash, hash: transactionHash } = rows[i];
43
+ const blockData = blocks.get(blockHash);
44
+
45
+ if (blockData === undefined) {
46
+ const firstEntry = new Map();
47
+ firstEntry.set(transactionHash, [rows[i]]);
48
+ blocks.set(blockHash, firstEntry);
49
+ } else {
50
+ const blockDataRows = blockData.get(transactionHash);
51
+ if (blockDataRows) {
52
+ blockDataRows.push(rows[i]);
53
+ } else {
54
+ blockData.set(transactionHash, [rows[i]]);
55
+ }
56
+ }
57
+ }
58
+ return blocks;
59
+ }
60
+
61
+ /**
62
+ * Extracts 'id' and 'field' properties from each row in the provided array and maps them together.
63
+ *
64
+ * This function iterates over each row in the given array, and for each row, it retrieves
65
+ * the 'id' and 'field' properties. It then stores these values in a Map where the key is
66
+ * the stringified 'id' and the value is the 'field'.
67
+ *
68
+ * @param rows An array of ArchiveNodeDatabaseRow objects to process.
69
+ * @returns A Map where the keys are the stringified 'id' properties from the input rows and
70
+ * the values are the corresponding 'field' properties.
71
+ */
72
+ function getElementIdFieldValues(rows: ArchiveNodeDatabaseRow[]) {
73
+ const elementIdValues: FieldElementIdWithValueMap = new Map();
74
+ for (let i = 0; i < rows.length; i++) {
75
+ const { field_id, field_value } = rows[i];
76
+ elementIdValues.set(field_id.toString(), field_value);
77
+ }
78
+ return elementIdValues;
79
+ }
80
+
81
+ function createUniqueEventId(
82
+ zkapp_account_update_id: number,
83
+ zkapp_event_array_id: number
84
+ ) {
85
+ return [zkapp_account_update_id, zkapp_event_array_id].join(',');
86
+ }
87
+
88
+ /**
89
+ * Removes redundant fields from an array of rows based on unique event and account update identifiers.
90
+ *
91
+ * There are redundant fields in the database output because a single action/event can be made up of multiple fields, which means
92
+ * we get rows with the same account update id and event array id but different field values.
93
+ *
94
+ * We remove rows that refer to the same event/action that is emitted, but just keep one row since
95
+ * one row can be used to derive all event/action field values (by it's element_ids).
96
+ *
97
+ * @example
98
+ * The following rows are returned from the database, which represents one event/action:
99
+ * [
100
+ * { id: 1, field: "1", element_ids: [3, 2, 1], zkapp_event_array_id: 1, zkapp_account_update_id: 1, zkapp_account_updates_ids: [1, 2, 3] },
101
+ * { id: 2, field: "2", element_ids: [3, 2, 1], zkapp_event_array_id: 1, zkapp_account_update_id: 1, zkapp_account_updates_ids: [1, 2, 3] },
102
+ * { id: 3, field: "3", element_ids: [3, 2, 1], zkapp_event_array_id: 1, zkapp_account_update_id: 1, zkapp_account_updates_ids: [1, 2, 3] },
103
+ * ]
104
+ *
105
+ * `id` corresponds to the field id, the `field` corresponds to the field value, and the `element_ids` correspond to the field ids. Since these rows
106
+ * represent a single event/action, they are all redundant because they have the same event array id and account update id.
107
+ *
108
+ * The output of this function will be:
109
+ * [
110
+ * { id: 1, field: "1", element_ids: [3, 2, 1], zkapp_event_array_id: 1, zkapp_account_update_id: 1, zkapp_account_updates_ids: [1, 2, 3] },
111
+ * ]
112
+ *
113
+ *
114
+ * @param archiveNodeRow An array of ArchiveNodeDatabaseRow objects to process.
115
+ * @returns A new array of ArchiveNodeDatabaseRow objects where redundant fields have been removed.
116
+ * @throws {Error} If no matching account update is found for a given account update id and event array id.
117
+ */
118
+ function removeRedundantEmittedFields(
119
+ archiveNodeRow: ArchiveNodeDatabaseRow[]
120
+ ) {
121
+ const inOrderTransactionRows: ArchiveNodeDatabaseRow[][] = [];
122
+ const seenEventOrActionIds = new Set<string>();
123
+ for (let i = 0; i < archiveNodeRow.length; i++) {
124
+ const currentRow = archiveNodeRow[i];
125
+ const {
126
+ event_field_elements_id, // The unique id for the field array in the current row
127
+ event_element_ids, // The list of element ids in the event (list of event_field_elements_id)
128
+ zkapp_account_update_id, // The unique id for the account update that emitted the event/action
129
+ zkapp_account_updates_ids, // List of all account update ids inside the transaction
130
+ } = currentRow;
131
+
132
+ // Create a unique Id consisting of the account update id and event/action id emitted
133
+ // This is used to check if we have already seen this event/action before.
134
+ const uniqueEventId = createUniqueEventId(
135
+ zkapp_account_update_id,
136
+ event_field_elements_id
137
+ );
138
+
139
+ if (!seenEventOrActionIds.has(uniqueEventId)) {
140
+ // Since multiple events/actions can be emitted in a single account update, we want to put back the event/action
141
+ // in the correct place. To do this, we need to know the index of the event array id in the list of event array ids (these stored in order by the Archive Node)
142
+ const emittedEventOrActionIndexes = findAllIndexes(
143
+ event_element_ids,
144
+ event_field_elements_id
145
+ );
146
+
147
+ // Since multiple account updates can be emitted in a single transaction, we need to know the index of the account update id in the list of account update ids
148
+ const accountUpdateIndexes = findAllIndexes(
149
+ zkapp_account_updates_ids,
150
+ zkapp_account_update_id
151
+ );
152
+
153
+ if (accountUpdateIndexes.length === 0) {
154
+ throw new Error(
155
+ `No matching account update found for the given account update ID (${zkapp_account_update_id}) and event array ID (${event_field_elements_id}).`
156
+ );
157
+ }
158
+
159
+ const accountUpdateIdIndex = accountUpdateIndexes[0];
160
+
161
+ // Put the event/action back in the correct place. The specific event/action should go into the index of the account update that emitted it.
162
+ // When we put this all together, the transaction will be in the correct order.
163
+ emittedEventOrActionIndexes.forEach((eventOrActionIndex) => {
164
+ if (inOrderTransactionRows[accountUpdateIdIndex] === undefined) {
165
+ inOrderTransactionRows[accountUpdateIdIndex] = [];
166
+ }
167
+ inOrderTransactionRows[accountUpdateIdIndex][eventOrActionIndex] =
168
+ currentRow;
169
+ });
170
+ seenEventOrActionIds.add(uniqueEventId);
171
+ }
172
+ }
173
+ return inOrderTransactionRows.flat();
174
+ }
175
+ /**
176
+ * Maps an array of database rows into an array of Action or Event instances.
177
+ *
178
+ * This function iterates over the given array of rows and for each row, it retrieves the element
179
+ * ids. It then looks up each element id in the provided map of field values and appends the corresponding
180
+ * value to an array, which is used to create a new Action or Event instance, depending on the
181
+ * provided kind.
182
+ *
183
+ * If the kind is 'event', a TransactionInfo object is created from the current row, and this information
184
+ * along with the array of values is used to create a new Event instance. If the kind is 'action',
185
+ * in addition to the TransactionInfo, the account update id from the current row is also used to
186
+ * create a new Action instance. The created instances are appended to the output array.
187
+ *
188
+ * If the element id does not exist in the provided map, the function simply continues with the next id.
189
+ *
190
+ * @param kind The type of instances to create. Can be either 'action' or 'event'.
191
+ * @param rows An array of ArchiveNodeDatabaseRow objects to map into Action or Event instances.
192
+ * @param elementIdFieldValues A map of element ids to their corresponding field values.
193
+ * @returns An array of Action or Event instances.
194
+ */
195
+ function mapActionOrEvent(
196
+ kind: 'action' | 'event',
197
+ rows: ArchiveNodeDatabaseRow[],
198
+ elementIdFieldValues: FieldElementIdWithValueMap
199
+ ) {
200
+ const data: (Event | Action)[] = [];
201
+ for (let i = 0; i < rows.length; i++) {
202
+ const { zkapp_account_update_id, event_field_element_ids } = rows[i];
203
+ const transactionInfo = createTransactionInfo(rows[i]);
204
+ const elementIdToFieldValues = getFieldValuesFromElementIds(
205
+ event_field_element_ids,
206
+ elementIdFieldValues
207
+ );
208
+
209
+ if (kind === 'event') {
210
+ const event = createEvent(
211
+ zkapp_account_update_id.toString(),
212
+ elementIdToFieldValues,
213
+ transactionInfo
214
+ );
215
+ data.push(event);
216
+ } else {
217
+ const { zkapp_account_update_id } = rows[i];
218
+ const action = createAction(
219
+ zkapp_account_update_id.toString(),
220
+ elementIdToFieldValues,
221
+ transactionInfo
222
+ );
223
+ data.push(action);
224
+ }
225
+ }
226
+ return data;
227
+ }
228
+
229
+ function getFieldValuesFromElementIds(
230
+ element_ids: number[],
231
+ elementIdFieldValues: FieldElementIdWithValueMap
232
+ ) {
233
+ const elementIdToFieldValues = [];
234
+ for (const elementId of element_ids) {
235
+ const elementIdFieldValue = elementIdFieldValues.get(elementId.toString());
236
+ if (elementIdFieldValue === undefined) continue;
237
+ elementIdToFieldValues.push(elementIdFieldValue);
238
+ }
239
+ return elementIdToFieldValues;
240
+ }
241
+
242
+ function sortAndFilterBlocks<T extends { blockInfo: BlockInfo }>(data: T[]) {
243
+ data.sort((a, b) => {
244
+ if (a.blockInfo.height < b.blockInfo.height) return -1;
245
+ if (a.blockInfo.height > b.blockInfo.height) return 1;
246
+ if (a.blockInfo.timestamp < b.blockInfo.timestamp) return -1;
247
+ if (a.blockInfo.timestamp > b.blockInfo.timestamp) return 1;
248
+ return 0;
249
+ });
250
+ filterBestTip(data);
251
+ }
252
+
253
+ function findAllIndexes<T>(arr: T[], target: T): number[] {
254
+ const indexes: number[] = [];
255
+ arr.forEach((element, index) => {
256
+ if (element === target) {
257
+ indexes.push(index);
258
+ }
259
+ });
260
+ return indexes;
261
+ }
@@ -0,0 +1,6 @@
1
+ import { Events } from '../../blockchain/types.js';
2
+ import { EventFilterOptionsInput } from '../../resolvers-types.js';
3
+
4
+ export interface IEventsService {
5
+ getEvents(input: EventFilterOptionsInput, options: unknown): Promise<Events>;
6
+ }