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