@subsquid/solana-normalization 0.0.4 → 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,396 +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 if (this.tx.err &&
354
- 'InstructionError' in this.tx.err &&
355
- (this.tx.err.InstructionError as [number, string])[1] === 'CallDepth') {
356
- } else {
357
- throw this.error(false, pos, 'invoke message is missing')
358
- }
359
- })
360
- }
361
110
 
362
- private logLessTraversal(
363
- parentStackHeight: number,
364
- instructions: rpc.Instruction[],
365
- pos: number,
366
- cb?: (ins: Instruction, pos: number) => void
367
- ): number {
368
- while (pos < instructions.length) {
369
- let instruction = instructions[pos]
370
- let stackHeight = instruction.stackHeight ?? 2
371
- if (stackHeight > parentStackHeight) {
372
- let ins = this.push(stackHeight, instruction)
373
- cb?.(ins, pos)
374
- pos += 1
375
- } else {
376
- return pos
377
- }
378
- }
379
- return pos
111
+ this.transactions.push(mapped)
112
+ this.balances.push(...mapBalances(ctx))
113
+ this.tokenBalances.push(...mapTokenBalances(ctx))
380
114
  }
381
115
 
382
- private push(stackHeight: number, src: rpc.Instruction): Instruction {
383
- 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]
384
119
 
385
- if (src.stackHeight != null) {
386
- assert(stackHeight === src.stackHeight)
387
- }
388
-
389
- let address = this.lastAddress.slice()
390
-
391
- while (address.length > stackHeight) {
392
- address.pop()
393
- }
394
-
395
- if (address.length === stackHeight) {
396
- address[stackHeight - 1] += 1
397
- } else {
398
- assert(address.length + 1 == stackHeight)
399
- address[stackHeight - 1] = 0
400
- }
120
+ let inner = ctx.tx.meta.innerInstructions?.flatMap(pack => {
121
+ return pack.index === i ? pack.instructions : []
122
+ })
401
123
 
402
- let i: Instruction = {
403
- transactionIndex: this.tx.transactionIndex,
404
- instructionAddress: address,
405
- programId: this.getAccount(src.programIdIndex),
406
- accounts: src.accounts.map(a => this.getAccount(a)),
407
- data: src.data,
408
- isCommitted: !this.tx.err,
409
- hasDroppedLogMessages: this.messagesTruncated
124
+ new InstructionTreeTraversal(
125
+ ctx,
126
+ messages,
127
+ i,
128
+ ins,
129
+ inner ?? [],
130
+ this.instructions,
131
+ this.logs
132
+ )
410
133
  }
411
-
412
- this.instructions.push(i)
413
- this.lastAddress = address
414
- return i
415
134
  }
135
+ }
416
136
 
417
- private assert(ok: unknown, withMessagePos: boolean, innerPos: number, msg: string): asserts ok {
418
- if (!ok) throw this.error(withMessagePos, innerPos, msg)
419
- }
420
137
 
421
- private error(withMessagePos: boolean, innerPos: number, msg: string): Error {
422
- let loc = `stopped at instruction ${this.pos}`
423
- if (innerPos > 0) {
424
- loc += `, inner instruction ${innerPos - 1})`
425
- }
426
- if (withMessagePos && this.messagePos < this.messages.length) {
427
- loc += ` and log message ${this.messagePos}`
428
- }
429
- return new Error(
430
- `Failed to process transaction ${this.tx.signatures[0]}: ${loc}: ${msg}`
431
- )
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
432
154
  }
433
155
  }
434
156
 
435
157
 
436
- function mapBalances(
437
- getAccount: (idx: number) => Base58Bytes,
438
- transactionIndex: number,
439
- tx: rpc.Transaction,
440
- ): Balance[] {
158
+ function mapBalances(ctx: TransactionContext): Balance[] {
441
159
  let balances: Balance[] = []
442
160
 
443
- let pre = tx.meta.preBalances
444
- let post = tx.meta.postBalances
161
+ let pre = ctx.tx.meta.preBalances
162
+ let post = ctx.tx.meta.postBalances
445
163
 
446
164
  assert(pre.length == post.length)
447
165
 
@@ -450,8 +168,8 @@ function mapBalances(
450
168
  // nothing changed, don't create an entry
451
169
  } else {
452
170
  balances.push({
453
- transactionIndex,
454
- account: getAccount(i),
171
+ transactionIndex: ctx.transactionIndex,
172
+ account: ctx.getAccount(i),
455
173
  pre: BigInt(pre[i]),
456
174
  post: BigInt(post[i])
457
175
  })
@@ -468,26 +186,22 @@ function mapBalances(
468
186
  }
469
187
 
470
188
 
471
- function mapTokenBalances(
472
- getAccount: (idx: number) => Base58Bytes,
473
- transactionIndex: number,
474
- tx: rpc.Transaction
475
- ): TokenBalance[] {
189
+ function mapTokenBalances(ctx: TransactionContext): TokenBalance[] {
476
190
  let balances: TokenBalance[] = []
477
191
 
478
192
  let preBalances = new Map(
479
- tx.meta.preTokenBalances?.map(b => [getAccount(b.accountIndex), b])
193
+ ctx.tx.meta.preTokenBalances?.map(b => [ctx.getAccount(b.accountIndex), b])
480
194
  )
481
195
 
482
196
  let postBalances = new Map(
483
- tx.meta.postTokenBalances?.map(b => [getAccount(b.accountIndex), b])
197
+ ctx.tx.meta.postTokenBalances?.map(b => [ctx.getAccount(b.accountIndex), b])
484
198
  )
485
199
 
486
200
  for (let [account, post] of postBalances.entries()) {
487
201
  let pre = preBalances.get(account)
488
202
  if (pre) {
489
203
  balances.push({
490
- transactionIndex,
204
+ transactionIndex: ctx.transactionIndex,
491
205
  account,
492
206
 
493
207
  preProgramId: pre.programId ?? undefined,
@@ -504,7 +218,7 @@ function mapTokenBalances(
504
218
  })
505
219
  } else {
506
220
  balances.push({
507
- transactionIndex,
221
+ transactionIndex: ctx.transactionIndex,
508
222
  account,
509
223
  postProgramId: post.programId ?? undefined,
510
224
  postMint: post.mint,
@@ -518,7 +232,7 @@ function mapTokenBalances(
518
232
  for (let [account, pre] of preBalances.entries()) {
519
233
  if (postBalances.has(account)) continue
520
234
  balances.push({
521
- transactionIndex,
235
+ transactionIndex: ctx.transactionIndex,
522
236
  account,
523
237
  preProgramId: pre.programId ?? undefined,
524
238
  preMint: pre.mint,