@subsquid/solana-normalization 0.0.3 → 1.0.0-portal-api.d887ad

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.
@@ -0,0 +1,76 @@
1
+ import {GetBlock} from '@subsquid/solana-rpc-data'
2
+ import {toJSON} from '@subsquid/util-internal-json'
3
+ import {fixUnsafeIntegers} from '@subsquid/util-internal-json-fix-unsafe-integers'
4
+ import assert, {fail} from 'assert'
5
+ import * as fs from 'fs'
6
+ import {it} from 'node:test'
7
+ import * as Path from 'path'
8
+ import {Journal, mapRpcBlock} from './mapping'
9
+ import {removeVotes} from './votes'
10
+
11
+
12
+ const FIXTURES_DIR = Path.resolve(__dirname, '../fixtures')
13
+
14
+
15
+ interface Fixture {
16
+ name: string
17
+ block: GetBlock
18
+ result: any
19
+ }
20
+
21
+
22
+ function* listFixtures(): Iterable<Fixture> {
23
+ for (let name of fs.readdirSync(FIXTURES_DIR)) {
24
+ let block: GetBlock
25
+ let result: any
26
+ try {
27
+ block = JSON.parse(
28
+ fixUnsafeIntegers(fs.readFileSync(Path.join(FIXTURES_DIR, name, 'block.json'), 'utf-8'))
29
+ )
30
+ result = JSON.parse(
31
+ fs.readFileSync(Path.join(FIXTURES_DIR, name, 'result.json'), 'utf-8')
32
+ )
33
+ } catch(err: any) {
34
+ if (err.code === 'ENOENT' || err.code == 'ENOTDIR') {
35
+ continue
36
+ } else {
37
+ throw err
38
+ }
39
+ }
40
+ yield {name, block, result}
41
+ }
42
+ }
43
+
44
+
45
+ const failingJournal: Journal = {
46
+ warn(props: any, msg: string) {
47
+ throw new Error(`got warning: ${msg}, props: ${JSON.stringify(props)}`)
48
+ },
49
+ error(props: any, msg: string) {
50
+ throw new Error(`got error: ${msg}, props: ${JSON.stringify(props)}`)
51
+ }
52
+ }
53
+
54
+
55
+ for (let fix of listFixtures()) {
56
+ it(fix.name, () => {
57
+ let result = mapRpcBlock(0, fix.block, failingJournal)
58
+ removeVotes(result)
59
+ let resultJson = normalizeJson(result)
60
+ try {
61
+ assert.deepStrictEqual(resultJson, fix.result)
62
+ } catch(err: any) {
63
+ // diff, that comes from deepStrictEqual is not useful for large objects
64
+ fs.writeFileSync(
65
+ Path.join(FIXTURES_DIR, fix.name, 'result.temp.json'),
66
+ JSON.stringify(resultJson, null, 2)
67
+ )
68
+ fail('result is different from a reference')
69
+ }
70
+ })
71
+ }
72
+
73
+
74
+ function normalizeJson(obj: any): any {
75
+ return JSON.parse(JSON.stringify(toJSON(obj)))
76
+ }
package/src/mapping.ts CHANGED
@@ -1,38 +1,28 @@
1
1
  import type * as rpc from '@subsquid/solana-rpc-data'
2
- import type {Base58Bytes} from '@subsquid/solana-rpc-data'
3
- import {addErrorContext, unexpectedCase} from '@subsquid/util-internal'
2
+ import {addErrorContext, assertNotNull} from '@subsquid/util-internal'
4
3
  import assert from 'assert'
5
4
  import {Balance, Block, BlockHeader, Instruction, LogMessage, Reward, TokenBalance, Transaction} from './data'
6
- import {InvokeMessage, InvokeResultMessage, LogTruncatedMessage, Message, parseLogMessage} from './log-parser'
5
+ import {InstructionTreeTraversal, MessageStream, ParsingError} from './instruction-parser'
6
+ import {LogTruncatedMessage} from './log-parser'
7
+ import {Journal, TransactionContext} from './transaction-context'
7
8
 
8
9
 
9
- export function mapRpcBlock(src: rpc.Block): Block {
10
+ export {Journal}
11
+
12
+
13
+ export function mapRpcBlock(slot: number, src: rpc.GetBlock, journal: Journal): Block {
10
14
  let header: BlockHeader = {
11
- hash: src.hash,
12
- height: src.height,
13
- slot: src.slot,
14
- parentSlot: src.block.parentSlot,
15
- parentHash: src.block.previousBlockhash,
16
- timestamp: src.block.blockTime ?? 0
15
+ hash: src.blockhash,
16
+ height: assertNotNull(src.blockHeight, '.blockHeight is not available'),
17
+ number: slot,
18
+ parentNumber: src.parentSlot,
19
+ parentHash: src.previousBlockhash,
20
+ timestamp: src.blockTime ?? 0
17
21
  }
18
22
 
19
- let instructions: Instruction[] = []
20
- let logs: LogMessage[] = []
21
- let balances: Balance[] = []
22
- let tokenBalances: TokenBalance[] = []
23
-
24
- let transactions = src.block.transactions
25
- ?.map((tx, i) => {
26
- try {
27
- return mapRpcTransaction(i, tx, instructions, logs, balances, tokenBalances)
28
- } catch(err: any) {
29
- throw addErrorContext(err, {
30
- blockTransaction: tx.transaction.signatures[0]
31
- })
32
- }
33
- }) ?? []
23
+ let items = new ItemMapping(journal, src)
34
24
 
35
- let rewards = src.block.rewards?.map(s => {
25
+ let rewards = src.rewards?.map(s => {
36
26
  let reward: Reward = {
37
27
  pubkey: s.pubkey,
38
28
  lamports: BigInt(s.lamports),
@@ -52,393 +42,124 @@ export function mapRpcBlock(src: rpc.Block): Block {
52
42
 
53
43
  return {
54
44
  header,
55
- transactions,
56
- instructions,
57
- logs,
58
- balances,
59
- tokenBalances,
45
+ transactions: items.transactions,
46
+ instructions: items.instructions,
47
+ logs: items.logs,
48
+ balances: items.balances,
49
+ tokenBalances: items.tokenBalances,
60
50
  rewards: rewards || []
61
51
  }
62
52
  }
63
53
 
64
54
 
65
- function mapRpcTransaction(
66
- transactionIndex: number,
67
- src: rpc.Transaction,
68
- instructions: Instruction[],
69
- logs: LogMessage[],
70
- balances: Balance[],
71
- tokenBalances: TokenBalance[]
72
- ): Transaction {
73
- let tx: Transaction = {
74
- transactionIndex,
75
- version: src.version,
76
- accountKeys: src.transaction.message.accountKeys,
77
- addressTableLookups: src.transaction.message.addressTableLookups ?? [],
78
- numReadonlySignedAccounts: src.transaction.message.header.numReadonlySignedAccounts,
79
- numReadonlyUnsignedAccounts: src.transaction.message.header.numReadonlyUnsignedAccounts,
80
- numRequiredSignatures: src.transaction.message.header.numRequiredSignatures,
81
- recentBlockhash: src.transaction.message.recentBlockhash,
82
- signatures: src.transaction.signatures,
83
- err: src.meta.err,
84
- computeUnitsConsumed: BigInt(src.meta.computeUnitsConsumed ?? 0),
85
- fee: BigInt(src.meta.fee),
86
- loadedAddresses: src.meta.loadedAddresses ?? {readonly: [], writable: []},
87
- hasDroppedLogMessages: false
88
- }
89
-
90
- let accounts: Base58Bytes[]
91
- if (tx.version === 'legacy') {
92
- accounts = tx.accountKeys
93
- } else {
94
- assert(src.meta?.loadedAddresses)
95
- accounts = tx.accountKeys.concat(
96
- src.meta.loadedAddresses.writable,
97
- src.meta.loadedAddresses.readonly
98
- )
99
- }
100
-
101
- let getAccount = (index: number): Base58Bytes => {
102
- assert(index < accounts.length)
103
- return accounts[index]
104
- }
105
-
106
- new InstructionParser(
107
- getAccount,
108
- tx,
109
- src,
110
- src.meta.logMessages?.map(parseLogMessage),
111
- instructions,
112
- logs
113
- ).parse()
114
-
115
- balances.push(
116
- ...mapBalances(getAccount, transactionIndex, src)
117
- )
118
-
119
- tokenBalances.push(
120
- ...mapTokenBalances(getAccount, transactionIndex, src)
121
- )
122
-
123
- return tx
124
- }
125
-
126
-
127
- const PROGRAMS_MISSING_INVOKE_LOG = new Set([
128
- 'AddressLookupTab1e1111111111111111111111111',
129
- 'BPFLoader1111111111111111111111111111111111',
130
- 'BPFLoader2111111111111111111111111111111111',
131
- 'BPFLoaderUpgradeab1e11111111111111111111111',
132
- 'Ed25519SigVerify111111111111111111111111111',
133
- 'KeccakSecp256k11111111111111111111111111111',
134
- 'NativeLoader1111111111111111111111111111111',
135
- 'ZkTokenProof1111111111111111111111111111111',
136
- ])
137
-
138
-
139
- class InstructionParser {
140
- private pos = 0
141
- private messages: Message[]
142
- private messagePos = 0
143
- private messagesTruncated = false
144
- private errorPos?: number
145
- private lastAddress: number[] = []
55
+ class ItemMapping {
56
+ transactions: Transaction[] = []
57
+ instructions: Instruction[] = []
58
+ logs: LogMessage[] = []
59
+ balances: Balance[] = []
60
+ tokenBalances: TokenBalance[] = []
146
61
 
147
62
  constructor(
148
- private getAccount: (index: number) => Base58Bytes,
149
- private tx: Transaction,
150
- private src: rpc.Transaction,
151
- messages: Message[] | undefined,
152
- private instructions: Instruction[],
153
- private logs: LogMessage[]
63
+ private journal: Journal,
64
+ block: rpc.GetBlock
154
65
  ) {
155
- if (messages == null) {
156
- this.messages = []
157
- this.messagesTruncated = true
158
- } else {
159
- this.messages = messages
160
- }
161
- let err: any = this.src.meta.err
162
- if (err) {
163
- if ('InstructionError' in err) {
164
- let pos = err['InstructionError'][0]
165
- assert(typeof pos == 'number')
166
- assert(0 <= pos && pos < this.src.transaction.message.instructions.length)
167
- this.errorPos = pos
66
+ let transactions = block.transactions ?? []
67
+ for (let i = 0; i < transactions.length; i++) {
68
+ let tx = transactions[i]
69
+ try {
70
+ this.processTransaction(i, tx)
71
+ } catch(err: any) {
72
+ throw addErrorContext(err, {
73
+ transactionHash: tx.transaction.signatures[0]
74
+ })
168
75
  }
169
76
  }
170
77
  }
171
78
 
172
- parse(): void {
173
- while (this.pos < this.src.transaction.message.instructions.length) {
174
- let instruction = this.src.transaction.message.instructions[this.pos]
175
-
176
- let inner = this.src.meta.innerInstructions
177
- ?.filter(i => i.index === this.pos)
178
- .flatMap(i => i.instructions) ?? []
179
-
180
- if (this.errorPos == null || this.errorPos >= this.pos) {
181
- let instructions = [instruction].concat(inner)
182
- let end = this.traverse(instructions, 0, 1)
183
- assert(end == instructions.length)
79
+ private processTransaction(transactionIndex: number, src: rpc.Transaction): void {
80
+ let mapped = mapTransaction(transactionIndex, src)
81
+
82
+ let ctx = new TransactionContext(transactionIndex, src, this.journal)
83
+
84
+ let messages = new MessageStream(src.meta.logMessages ?? [])
85
+
86
+ let insCheckPoint = this.instructions.length
87
+ let logCheckPoint = this.logs.length
88
+ try {
89
+ this.traverseInstructions(ctx, messages)
90
+ mapped.hasDroppedLogMessages = messages.truncated
91
+ } catch(err: any) {
92
+ if (err instanceof ParsingError) {
93
+ // report parsing problem
94
+ let {msg, ...props} = err
95
+ ctx.error(props, msg)
96
+ // reparse without log messages
97
+ mapped.hasDroppedLogMessages = true
98
+ // restore state before failed traversal
99
+ this.instructions = this.instructions.slice(0, insCheckPoint)
100
+ this.logs = this.logs.slice(0, logCheckPoint)
101
+ // traverse again with dummy truncated MessageStream
102
+ this.traverseInstructions(
103
+ ctx,
104
+ new MessageStream([new LogTruncatedMessage().toString()])
105
+ )
184
106
  } else {
185
- this.assert(inner.length == 0, false, 0, 'seemingly non-executed instruction has inner instructions')
186
- this.push(1, instruction)
187
- }
188
-
189
- this.pos += 1
190
- }
191
- this.tx.hasDroppedLogMessages = this.tx.hasDroppedLogMessages || this.messagesTruncated
192
- }
193
-
194
- private traverse(
195
- instructions: rpc.Instruction[],
196
- pos: number,
197
- stackHeight: number
198
- ): number {
199
- this.assert(pos < instructions.length, true, 0, 'unexpected and of inner instructions')
200
-
201
- let instruction = instructions[pos]
202
-
203
- this.assert(
204
- instruction.stackHeight == null || instruction.stackHeight == stackHeight,
205
- false, pos, 'instruction has unexpected stack height'
206
- )
207
-
208
- let msg: Message | undefined
209
- if (this.messagePos < this.messages.length) {
210
- msg = this.messages[this.messagePos]
211
- }
212
-
213
- if (msg?.kind == 'truncate') {
214
- this.messagePos += 1
215
- this.messagesTruncated = true
216
- }
217
-
218
- if (this.messagesTruncated) {
219
- this.push(stackHeight, instruction)
220
- return this.logLessTraversal(stackHeight, instructions, pos + 1)
221
- }
222
-
223
- let programId = this.getAccount(instruction.programIdIndex)
224
-
225
- if (msg?.kind === 'invoke' && msg.programId === programId) {
226
- this.assert(msg.stackHeight == stackHeight, true, pos, 'invoke message has unexpected stack height')
227
- this.messagePos += 1
228
- return this.invokeInstruction(stackHeight, instructions, pos)
229
- }
230
-
231
- if (PROGRAMS_MISSING_INVOKE_LOG.has(programId)) {
232
- let dropped = this.dropInvokeLessInstructionMessages(pos)
233
- let ins = this.push(stackHeight, instruction)
234
- ins.hasDroppedLogMessages = ins.hasDroppedLogMessages || dropped
235
- this.tx.hasDroppedLogMessages = this.tx.hasDroppedLogMessages || dropped
236
- return this.invokeLessTraversal(dropped, stackHeight, instructions, pos + 1)
237
- }
238
-
239
- // FIXME: add an option to ignore this
240
- throw this.error(true, pos, 'missing invoke message')
241
- }
242
-
243
- private dropInvokeLessInstructionMessages(pos: number): boolean {
244
- let initialPos = this.messagePos
245
- while (this.messagePos < this.messages.length && !this.messagesTruncated) {
246
- let msg = this.messages[this.messagePos]
247
- switch(msg.kind) {
248
- case 'log':
249
- case 'data':
250
- case 'cu':
251
- case 'other':
252
- this.messagePos += 1
253
- break
254
- case 'truncate':
255
- this.messagePos += 1
256
- this.messagesTruncated = true
257
- return true
258
- case 'invoke':
259
- return this.messagePos - initialPos > 0
260
- case 'invoke-result':
261
- throw this.error(true, pos, `invoke result message does not match any invoke`)
262
- default:
263
- throw unexpectedCase()
264
- }
265
- }
266
- return false
267
- }
268
-
269
- private invokeInstruction(
270
- stackHeight: number,
271
- instructions: rpc.Instruction[],
272
- instructionPos: number
273
- ): number {
274
- let ins = this.push(stackHeight, instructions[instructionPos])
275
- let pos = instructionPos + 1
276
- while (true) {
277
- let token = this.takeInstructionMessages(ins, instructionPos)
278
- switch(token.kind) {
279
- case 'invoke':
280
- pos = this.traverse(instructions, pos, stackHeight + 1)
281
- break
282
- case 'invoke-result':
283
- if (token.programId != ins.programId) {
284
- throw this.error(true, instructionPos,
285
- `invoke result message and instruction program ids don't match`
286
- )
287
- }
288
- if (token.error) {
289
- ins.error = token.error
290
- }
291
- pos = this.invokeLessTraversal(true, stackHeight, instructions, pos)
292
- this.messagePos += 1
293
- return pos
294
- case 'truncate':
295
- ins.hasDroppedLogMessages = true
296
- return this.logLessTraversal(stackHeight, instructions, pos)
297
- default:
298
- throw unexpectedCase()
107
+ throw err
299
108
  }
300
109
  }
301
- }
302
-
303
- private takeInstructionMessages(
304
- ins: Instruction,
305
- pos: number
306
- ): InvokeMessage | InvokeResultMessage | LogTruncatedMessage {
307
- if (this.messagesTruncated) return new LogTruncatedMessage()
308
- while (this.messagePos < this.messages.length) {
309
- let msg = this.messages[this.messagePos]
310
- switch(msg.kind) {
311
- case 'log':
312
- case 'data':
313
- case 'other':
314
- this.logs.push({
315
- transactionIndex: ins.transactionIndex,
316
- logIndex: this.messagePos,
317
- instructionAddress: ins.instructionAddress,
318
- programId: ins.programId,
319
- kind: msg.kind,
320
- message: msg.message
321
- })
322
- break
323
- case 'cu':
324
- if (ins.programId != msg.programId) {
325
- throw this.error(true, pos, 'unexpected programId in compute unit message')
326
- }
327
- ins.computeUnitsConsumed = msg.consumed
328
- break
329
- case 'invoke':
330
- case 'invoke-result':
331
- return msg
332
- case 'truncate':
333
- this.messagesTruncated = true
334
- this.messagePos += 1
335
- return msg
336
- default:
337
- throw unexpectedCase()
338
- }
339
- this.messagePos += 1
340
- }
341
- throw this.error(false, pos, 'unexpected end of log messages')
342
- }
343
-
344
- private invokeLessTraversal(
345
- messagesDropped: boolean,
346
- parentStackHeight: number,
347
- instructions: rpc.Instruction[],
348
- pos: number
349
- ): number {
350
- return this.logLessTraversal(parentStackHeight, instructions, pos, (ins, pos) => {
351
- ins.hasDroppedLogMessages = ins.hasDroppedLogMessages || messagesDropped
352
- if (PROGRAMS_MISSING_INVOKE_LOG.has(ins.programId)) {
353
- } else {
354
- throw this.error(false, pos, 'invoke message is missing')
355
- }
356
- })
357
- }
358
110
 
359
- private logLessTraversal(
360
- parentStackHeight: number,
361
- instructions: rpc.Instruction[],
362
- pos: number,
363
- cb?: (ins: Instruction, pos: number) => void
364
- ): number {
365
- while (pos < instructions.length) {
366
- let instruction = instructions[pos]
367
- let stackHeight = instruction.stackHeight ?? 2
368
- if (stackHeight > parentStackHeight) {
369
- let ins = this.push(stackHeight, instruction)
370
- cb?.(ins, pos)
371
- pos += 1
372
- } else {
373
- return pos
374
- }
375
- }
376
- return pos
111
+ this.transactions.push(mapped)
112
+ this.balances.push(...mapBalances(ctx))
113
+ this.tokenBalances.push(...mapTokenBalances(ctx))
377
114
  }
378
115
 
379
- private push(stackHeight: number, src: rpc.Instruction): Instruction {
380
- assert(stackHeight > 0)
116
+ private traverseInstructions(ctx: TransactionContext, messages: MessageStream): void {
117
+ for (let i = 0; i < ctx.tx.transaction.message.instructions.length; i++) {
118
+ let ins = ctx.tx.transaction.message.instructions[i]
381
119
 
382
- if (src.stackHeight != null) {
383
- assert(stackHeight === src.stackHeight)
384
- }
385
-
386
- let address = this.lastAddress.slice()
387
-
388
- while (address.length > stackHeight) {
389
- address.pop()
390
- }
391
-
392
- if (address.length === stackHeight) {
393
- address[stackHeight - 1] += 1
394
- } else {
395
- assert(address.length + 1 == stackHeight)
396
- address[stackHeight - 1] = 0
397
- }
120
+ let inner = ctx.tx.meta.innerInstructions?.flatMap(pack => {
121
+ return pack.index === i ? pack.instructions : []
122
+ })
398
123
 
399
- let i: Instruction = {
400
- transactionIndex: this.tx.transactionIndex,
401
- instructionAddress: address,
402
- programId: this.getAccount(src.programIdIndex),
403
- accounts: src.accounts.map(a => this.getAccount(a)),
404
- data: src.data,
405
- isCommitted: !this.tx.err,
406
- hasDroppedLogMessages: this.messagesTruncated
124
+ new InstructionTreeTraversal(
125
+ ctx,
126
+ messages,
127
+ i,
128
+ ins,
129
+ inner ?? [],
130
+ this.instructions,
131
+ this.logs
132
+ )
407
133
  }
408
-
409
- this.instructions.push(i)
410
- this.lastAddress = address
411
- return i
412
134
  }
135
+ }
413
136
 
414
- private assert(ok: unknown, withMessagePos: boolean, innerPos: number, msg: string): asserts ok {
415
- if (!ok) throw this.error(withMessagePos, innerPos, msg)
416
- }
417
137
 
418
- private error(withMessagePos: boolean, innerPos: number, msg: string): Error {
419
- let loc = `stopped at instruction ${this.pos}`
420
- if (innerPos > 0) {
421
- loc += `, inner instruction ${innerPos - 1})`
422
- }
423
- if (withMessagePos && this.messagePos < this.messages.length) {
424
- loc += ` and log message ${this.messagePos}`
425
- }
426
- return new Error(
427
- `Failed to process transaction ${this.tx.signatures[0]}: ${loc}: ${msg}`
428
- )
138
+ function mapTransaction(transactionIndex: number, src: rpc.Transaction): Transaction {
139
+ return {
140
+ transactionIndex,
141
+ version: src.version,
142
+ accountKeys: src.transaction.message.accountKeys,
143
+ addressTableLookups: src.transaction.message.addressTableLookups ?? [],
144
+ numReadonlySignedAccounts: src.transaction.message.header.numReadonlySignedAccounts,
145
+ numReadonlyUnsignedAccounts: src.transaction.message.header.numReadonlyUnsignedAccounts,
146
+ numRequiredSignatures: src.transaction.message.header.numRequiredSignatures,
147
+ recentBlockhash: src.transaction.message.recentBlockhash,
148
+ signatures: src.transaction.signatures,
149
+ err: src.meta.err,
150
+ computeUnitsConsumed: BigInt(src.meta.computeUnitsConsumed ?? 0),
151
+ fee: BigInt(src.meta.fee),
152
+ loadedAddresses: src.meta.loadedAddresses ?? {readonly: [], writable: []},
153
+ hasDroppedLogMessages: false
429
154
  }
430
155
  }
431
156
 
432
157
 
433
- function mapBalances(
434
- getAccount: (idx: number) => Base58Bytes,
435
- transactionIndex: number,
436
- tx: rpc.Transaction,
437
- ): Balance[] {
158
+ function mapBalances(ctx: TransactionContext): Balance[] {
438
159
  let balances: Balance[] = []
439
160
 
440
- let pre = tx.meta.preBalances
441
- let post = tx.meta.postBalances
161
+ let pre = ctx.tx.meta.preBalances
162
+ let post = ctx.tx.meta.postBalances
442
163
 
443
164
  assert(pre.length == post.length)
444
165
 
@@ -447,8 +168,8 @@ function mapBalances(
447
168
  // nothing changed, don't create an entry
448
169
  } else {
449
170
  balances.push({
450
- transactionIndex,
451
- account: getAccount(i),
171
+ transactionIndex: ctx.transactionIndex,
172
+ account: ctx.getAccount(i),
452
173
  pre: BigInt(pre[i]),
453
174
  post: BigInt(post[i])
454
175
  })
@@ -465,26 +186,22 @@ function mapBalances(
465
186
  }
466
187
 
467
188
 
468
- function mapTokenBalances(
469
- getAccount: (idx: number) => Base58Bytes,
470
- transactionIndex: number,
471
- tx: rpc.Transaction
472
- ): TokenBalance[] {
189
+ function mapTokenBalances(ctx: TransactionContext): TokenBalance[] {
473
190
  let balances: TokenBalance[] = []
474
191
 
475
192
  let preBalances = new Map(
476
- tx.meta.preTokenBalances?.map(b => [getAccount(b.accountIndex), b])
193
+ ctx.tx.meta.preTokenBalances?.map(b => [ctx.getAccount(b.accountIndex), b])
477
194
  )
478
195
 
479
196
  let postBalances = new Map(
480
- tx.meta.postTokenBalances?.map(b => [getAccount(b.accountIndex), b])
197
+ ctx.tx.meta.postTokenBalances?.map(b => [ctx.getAccount(b.accountIndex), b])
481
198
  )
482
199
 
483
200
  for (let [account, post] of postBalances.entries()) {
484
201
  let pre = preBalances.get(account)
485
202
  if (pre) {
486
203
  balances.push({
487
- transactionIndex,
204
+ transactionIndex: ctx.transactionIndex,
488
205
  account,
489
206
 
490
207
  preProgramId: pre.programId ?? undefined,
@@ -501,7 +218,7 @@ function mapTokenBalances(
501
218
  })
502
219
  } else {
503
220
  balances.push({
504
- transactionIndex,
221
+ transactionIndex: ctx.transactionIndex,
505
222
  account,
506
223
  postProgramId: post.programId ?? undefined,
507
224
  postMint: post.mint,
@@ -515,7 +232,7 @@ function mapTokenBalances(
515
232
  for (let [account, pre] of preBalances.entries()) {
516
233
  if (postBalances.has(account)) continue
517
234
  balances.push({
518
- transactionIndex,
235
+ transactionIndex: ctx.transactionIndex,
519
236
  account,
520
237
  preProgramId: pre.programId ?? undefined,
521
238
  preMint: pre.mint,