@subsquid/solana-normalization 0.0.0

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,146 @@
1
+ import type {Base58Bytes} from '@subsquid/solana-rpc-data'
2
+
3
+
4
+ export class InvokeMessage {
5
+ public kind = 'invoke' as const
6
+
7
+ constructor(
8
+ public programId: Base58Bytes,
9
+ public stackHeight: number
10
+ ) {}
11
+
12
+ static parse(msg: string): InvokeMessage | undefined {
13
+ let m = /^Program ([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+) invoke \[(\d+)]$/.exec(msg)
14
+ if (m) return new InvokeMessage(m[1], parseInt(m[2]))
15
+ }
16
+
17
+ toString(): string {
18
+ return `Program ${this.programId} invoke [${this.stackHeight}]`
19
+ }
20
+ }
21
+
22
+
23
+ export class InvokeResultMessage {
24
+ public kind = 'invoke-result' as const
25
+
26
+ constructor(
27
+ public programId: Base58Bytes,
28
+ public success: boolean,
29
+ public error?: string
30
+ ) {}
31
+
32
+ static parse(msg: string): InvokeResultMessage | undefined {
33
+ let m = /^Program ([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+) success$/.exec(msg)
34
+ if (m) return new InvokeResultMessage(m[1], true)
35
+
36
+ m = /^Program ([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+) failed: (.*)$/.exec(msg)
37
+ if (m) return new InvokeResultMessage(m[1], false, m[2])
38
+ }
39
+
40
+ toString(): string {
41
+ if (this.success) {
42
+ return `Program ${this.programId} success`
43
+ } else {
44
+ return `Program ${this.programId} failed: ${this.error}`
45
+ }
46
+ }
47
+ }
48
+
49
+
50
+ export class ProgramLogMessage {
51
+ public kind = 'log' as const
52
+
53
+ constructor(public message: string) {}
54
+
55
+ static parse(msg: string): ProgramLogMessage | undefined {
56
+ let m = /^Program log: (.*)$/.exec(msg)
57
+ if (m) return new ProgramLogMessage(m[1])
58
+ }
59
+
60
+ toString(): string {
61
+ return `Program log: ${this.message}`
62
+ }
63
+ }
64
+
65
+
66
+ export class ComputeUnitsMessage {
67
+ public kind = 'cu' as const
68
+
69
+ constructor(
70
+ public programId: Base58Bytes,
71
+ public consumed: bigint,
72
+ public available: bigint
73
+ ) {}
74
+
75
+ static parse(msg: string): ComputeUnitsMessage | undefined {
76
+ let m = /^Program ([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+) consumed (\d+) of (\d+) compute units$/.exec(msg)
77
+ if (m) return new ComputeUnitsMessage(m[1], BigInt(m[2]), BigInt(m[3]))
78
+ }
79
+
80
+ toString(): string {
81
+ return `Program ${this.programId} consumed ${this.consumed} of ${this.available} compute units`
82
+ }
83
+ }
84
+
85
+
86
+ export class ProgramDataMessage {
87
+ public kind = 'data' as const
88
+
89
+ constructor(
90
+ public message: string
91
+ ) {}
92
+
93
+ static parse(msg: string): ProgramDataMessage | undefined {
94
+ let m = /^Program data: (.*)$/.exec(msg)
95
+ if (m) return new ProgramDataMessage(m[1])
96
+ }
97
+
98
+ toString(): string {
99
+ return `Program data: ${this.message}`
100
+ }
101
+ }
102
+
103
+
104
+ export class LogTruncatedMessage {
105
+ public kind = 'truncate' as const
106
+
107
+ static parse(msg: string): LogTruncatedMessage | undefined {
108
+ if (msg == 'Log truncated') return new LogTruncatedMessage()
109
+ }
110
+
111
+ toString(): string {
112
+ return 'Log truncated'
113
+ }
114
+ }
115
+
116
+
117
+ export class OtherMessage {
118
+ public kind = 'other' as const
119
+
120
+ constructor(public message: string) {}
121
+
122
+ toString(): string {
123
+ return this.message
124
+ }
125
+ }
126
+
127
+
128
+ export type Message =
129
+ InvokeMessage |
130
+ InvokeResultMessage |
131
+ ProgramLogMessage |
132
+ ProgramDataMessage |
133
+ ComputeUnitsMessage |
134
+ LogTruncatedMessage |
135
+ OtherMessage
136
+
137
+
138
+ export function parseLogMessage(msg: string): Message {
139
+ return InvokeMessage.parse(msg)
140
+ || InvokeResultMessage.parse(msg)
141
+ || ProgramLogMessage.parse(msg)
142
+ || ProgramDataMessage.parse(msg)
143
+ || ComputeUnitsMessage.parse(msg)
144
+ || LogTruncatedMessage.parse(msg)
145
+ || new OtherMessage(msg)
146
+ }
package/src/mapping.ts ADDED
@@ -0,0 +1,535 @@
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'
4
+ import assert from 'assert'
5
+ import {Balance, Block, BlockHeader, Instruction, LogMessage, Reward, TokenBalance, Transaction} from './data'
6
+ import {InvokeMessage, InvokeResultMessage, LogTruncatedMessage, Message, parseLogMessage} from './log-parser'
7
+
8
+
9
+ export function mapRpcBlock(src: rpc.Block): Block {
10
+ 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
17
+ }
18
+
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
+ }) ?? []
34
+
35
+ let rewards = src.block.rewards?.map(s => {
36
+ let reward: Reward = {
37
+ pubkey: s.pubkey,
38
+ lamports: BigInt(s.lamports),
39
+ postBalance: BigInt(s.postBalance)
40
+ }
41
+
42
+ if (s.rewardType) {
43
+ reward.rewardType = s.rewardType
44
+ }
45
+
46
+ if (s.commission != null) {
47
+ reward.commission = s.commission
48
+ }
49
+
50
+ return reward
51
+ })
52
+
53
+ return {
54
+ header,
55
+ transactions,
56
+ instructions,
57
+ logs,
58
+ balances,
59
+ tokenBalances,
60
+ rewards: rewards || []
61
+ }
62
+ }
63
+
64
+
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[] = []
146
+
147
+ 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[]
154
+ ) {
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
168
+ }
169
+ }
170
+ }
171
+
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)
184
+ } 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()
299
+ }
300
+ }
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
+
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
377
+ }
378
+
379
+ private push(stackHeight: number, src: rpc.Instruction): Instruction {
380
+ assert(stackHeight > 0)
381
+
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
+ }
398
+
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
407
+ }
408
+
409
+ this.instructions.push(i)
410
+ this.lastAddress = address
411
+ return i
412
+ }
413
+
414
+ private assert(ok: unknown, withMessagePos: boolean, innerPos: number, msg: string): asserts ok {
415
+ if (!ok) throw this.error(withMessagePos, innerPos, msg)
416
+ }
417
+
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
+ )
429
+ }
430
+ }
431
+
432
+
433
+ function mapBalances(
434
+ getAccount: (idx: number) => Base58Bytes,
435
+ transactionIndex: number,
436
+ tx: rpc.Transaction,
437
+ ): Balance[] {
438
+ let balances: Balance[] = []
439
+
440
+ let pre = tx.meta.preBalances
441
+ let post = tx.meta.postBalances
442
+
443
+ assert(pre.length == post.length)
444
+
445
+ for (let i = 0; i < pre.length; i++) {
446
+ if (pre[i] === post[i]) {
447
+ // nothing changed, don't create an entry
448
+ } else {
449
+ balances.push({
450
+ transactionIndex,
451
+ account: getAccount(i),
452
+ pre: BigInt(pre[i]),
453
+ post: BigInt(post[i])
454
+ })
455
+ }
456
+ }
457
+
458
+ balances.sort((a, b) => {
459
+ if (a.account < b.account) return -1
460
+ if (a.account > b.account) return 1
461
+ return 0
462
+ })
463
+
464
+ return balances
465
+ }
466
+
467
+
468
+ function mapTokenBalances(
469
+ getAccount: (idx: number) => Base58Bytes,
470
+ transactionIndex: number,
471
+ tx: rpc.Transaction
472
+ ): TokenBalance[] {
473
+ let balances: TokenBalance[] = []
474
+
475
+ let preBalances = new Map(
476
+ tx.meta.preTokenBalances?.map(b => [getAccount(b.accountIndex), b])
477
+ )
478
+
479
+ let postBalances = new Map(
480
+ tx.meta.postTokenBalances?.map(b => [getAccount(b.accountIndex), b])
481
+ )
482
+
483
+ for (let [account, post] of postBalances.entries()) {
484
+ let pre = preBalances.get(account)
485
+ if (pre) {
486
+ balances.push({
487
+ transactionIndex,
488
+ account,
489
+
490
+ preProgramId: pre.programId ?? undefined,
491
+ preMint: pre.mint,
492
+ preDecimals: pre.uiTokenAmount.decimals,
493
+ preOwner: pre.owner ?? undefined,
494
+ preAmount: BigInt(pre.uiTokenAmount.amount),
495
+
496
+ postProgramId: post.programId ?? undefined,
497
+ postMint: post.mint,
498
+ postDecimals: post.uiTokenAmount.decimals,
499
+ postOwner: post.owner ?? undefined,
500
+ postAmount: BigInt(post.uiTokenAmount.amount)
501
+ })
502
+ } else {
503
+ balances.push({
504
+ transactionIndex,
505
+ account,
506
+ postProgramId: post.programId ?? undefined,
507
+ postMint: post.mint,
508
+ postDecimals: post.uiTokenAmount.decimals,
509
+ postOwner: post.owner ?? undefined,
510
+ postAmount: BigInt(post.uiTokenAmount.amount)
511
+ })
512
+ }
513
+ }
514
+
515
+ for (let [account, pre] of preBalances.entries()) {
516
+ if (postBalances.has(account)) continue
517
+ balances.push({
518
+ transactionIndex,
519
+ account,
520
+ preProgramId: pre.programId ?? undefined,
521
+ preMint: pre.mint,
522
+ preDecimals: pre.uiTokenAmount.decimals,
523
+ preOwner: pre.owner ?? undefined,
524
+ preAmount: BigInt(pre.uiTokenAmount.amount)
525
+ })
526
+ }
527
+
528
+ balances.sort((a, b) => {
529
+ if (a.account < b.account) return -1
530
+ if (a.account > b.account) return 1
531
+ return 0
532
+ })
533
+
534
+ return balances
535
+ }