@subsquid/solana-normalization 0.0.8 → 0.1.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.
- 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 -304
- 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 +2 -2
- package/src/instruction-parser.ts +337 -0
- package/src/mapping.ts +103 -388
- 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,395 +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
|
-
])
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
class InstructionParser {
|
|
143
|
-
private pos = 0
|
|
144
|
-
private messages: Message[]
|
|
145
|
-
private messagePos = 0
|
|
146
|
-
private messagesTruncated = false
|
|
147
|
-
private errorPos?: number
|
|
148
|
-
private lastAddress: number[] = []
|
|
53
|
+
class ItemMapping {
|
|
54
|
+
transactions: Transaction[] = []
|
|
55
|
+
instructions: Instruction[] = []
|
|
56
|
+
logs: LogMessage[] = []
|
|
57
|
+
balances: Balance[] = []
|
|
58
|
+
tokenBalances: TokenBalance[] = []
|
|
149
59
|
|
|
150
60
|
constructor(
|
|
151
|
-
private
|
|
152
|
-
|
|
153
|
-
private src: rpc.Transaction,
|
|
154
|
-
messages: Message[] | undefined,
|
|
155
|
-
private instructions: Instruction[],
|
|
156
|
-
private logs: LogMessage[]
|
|
61
|
+
private journal: Journal,
|
|
62
|
+
block: rpc.GetBlock
|
|
157
63
|
) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
assert(0 <= pos && pos < this.src.transaction.message.instructions.length)
|
|
169
|
-
this.errorPos = pos
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
parse(): void {
|
|
174
|
-
while (this.pos < this.src.transaction.message.instructions.length) {
|
|
175
|
-
let instruction = this.src.transaction.message.instructions[this.pos]
|
|
176
|
-
|
|
177
|
-
let inner = this.src.meta.innerInstructions
|
|
178
|
-
?.filter(i => i.index === this.pos)
|
|
179
|
-
.flatMap(i => i.instructions) ?? []
|
|
180
|
-
|
|
181
|
-
if (this.errorPos == null || this.errorPos >= this.pos) {
|
|
182
|
-
let instructions = [instruction].concat(inner)
|
|
183
|
-
let end = this.traverse(instructions, 0, 1)
|
|
184
|
-
assert(end == instructions.length)
|
|
185
|
-
} else {
|
|
186
|
-
this.assert(inner.length == 0, false, 0, 'seemingly non-executed instruction has inner instructions')
|
|
187
|
-
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
|
+
})
|
|
188
74
|
}
|
|
189
|
-
|
|
190
|
-
this.pos += 1
|
|
191
75
|
}
|
|
192
|
-
this.tx.hasDroppedLogMessages = this.tx.hasDroppedLogMessages || this.messagesTruncated
|
|
193
76
|
}
|
|
194
77
|
|
|
195
|
-
private
|
|
196
|
-
|
|
197
|
-
pos: number,
|
|
198
|
-
stackHeight: number
|
|
199
|
-
): number {
|
|
200
|
-
this.assert(pos < instructions.length, true, 0, 'unexpected and of inner instructions')
|
|
201
|
-
|
|
202
|
-
let instruction = instructions[pos]
|
|
78
|
+
private processTransaction(transactionIndex: number, src: rpc.Transaction): void {
|
|
79
|
+
let mapped = mapTransaction(transactionIndex, src)
|
|
203
80
|
|
|
204
|
-
this.
|
|
205
|
-
instruction.stackHeight == null || instruction.stackHeight == stackHeight,
|
|
206
|
-
false, pos, 'instruction has unexpected stack height'
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
let msg: Message | undefined
|
|
210
|
-
if (this.messagePos < this.messages.length) {
|
|
211
|
-
msg = this.messages[this.messagePos]
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (msg?.kind == 'truncate') {
|
|
215
|
-
this.messagePos += 1
|
|
216
|
-
this.messagesTruncated = true
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (this.messagesTruncated) {
|
|
220
|
-
this.push(stackHeight, instruction)
|
|
221
|
-
return this.logLessTraversal(stackHeight, instructions, pos + 1)
|
|
222
|
-
}
|
|
81
|
+
let ctx = new TransactionContext(transactionIndex, src, this.journal)
|
|
223
82
|
|
|
224
|
-
let
|
|
83
|
+
let messages = new MessageStream(src.meta.logMessages ?? [])
|
|
225
84
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
ins.hasDroppedLogMessages = ins.hasDroppedLogMessages || dropped
|
|
236
|
-
this.tx.hasDroppedLogMessages = this.tx.hasDroppedLogMessages || dropped
|
|
237
|
-
return this.invokeLessTraversal(dropped, stackHeight, instructions, pos + 1)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// FIXME: add an option to ignore this
|
|
241
|
-
throw this.error(true, pos, 'missing invoke message')
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
private dropInvokeLessInstructionMessages(pos: number): boolean {
|
|
245
|
-
let initialPos = this.messagePos
|
|
246
|
-
while (this.messagePos < this.messages.length && !this.messagesTruncated) {
|
|
247
|
-
let msg = this.messages[this.messagePos]
|
|
248
|
-
switch(msg.kind) {
|
|
249
|
-
case 'log':
|
|
250
|
-
case 'data':
|
|
251
|
-
case 'cu':
|
|
252
|
-
case 'other':
|
|
253
|
-
this.messagePos += 1
|
|
254
|
-
break
|
|
255
|
-
case 'truncate':
|
|
256
|
-
this.messagePos += 1
|
|
257
|
-
this.messagesTruncated = true
|
|
258
|
-
return true
|
|
259
|
-
case 'invoke':
|
|
260
|
-
return this.messagePos - initialPos > 0
|
|
261
|
-
case 'invoke-result':
|
|
262
|
-
throw this.error(true, pos, `invoke result message does not match any invoke`)
|
|
263
|
-
default:
|
|
264
|
-
throw unexpectedCase()
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
return false
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
private invokeInstruction(
|
|
271
|
-
stackHeight: number,
|
|
272
|
-
instructions: rpc.Instruction[],
|
|
273
|
-
instructionPos: number
|
|
274
|
-
): number {
|
|
275
|
-
let ins = this.push(stackHeight, instructions[instructionPos])
|
|
276
|
-
let pos = instructionPos + 1
|
|
277
|
-
while (true) {
|
|
278
|
-
let token = this.takeInstructionMessages(ins, instructionPos)
|
|
279
|
-
switch(token.kind) {
|
|
280
|
-
case 'invoke':
|
|
281
|
-
pos = this.traverse(instructions, pos, stackHeight + 1)
|
|
282
|
-
break
|
|
283
|
-
case 'invoke-result':
|
|
284
|
-
if (token.programId != ins.programId) {
|
|
285
|
-
throw this.error(true, instructionPos,
|
|
286
|
-
`invoke result message and instruction program ids don't match`
|
|
287
|
-
)
|
|
288
|
-
}
|
|
289
|
-
if (token.error) {
|
|
290
|
-
ins.error = token.error
|
|
291
|
-
}
|
|
292
|
-
pos = this.invokeLessTraversal(true, stackHeight, instructions, pos)
|
|
293
|
-
this.messagePos += 1
|
|
294
|
-
return pos
|
|
295
|
-
case 'truncate':
|
|
296
|
-
ins.hasDroppedLogMessages = true
|
|
297
|
-
return this.logLessTraversal(stackHeight, instructions, pos)
|
|
298
|
-
default:
|
|
299
|
-
throw unexpectedCase()
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
private takeInstructionMessages(
|
|
305
|
-
ins: Instruction,
|
|
306
|
-
pos: number
|
|
307
|
-
): InvokeMessage | InvokeResultMessage | LogTruncatedMessage {
|
|
308
|
-
if (this.messagesTruncated) return new LogTruncatedMessage()
|
|
309
|
-
while (this.messagePos < this.messages.length) {
|
|
310
|
-
let msg = this.messages[this.messagePos]
|
|
311
|
-
switch(msg.kind) {
|
|
312
|
-
case 'log':
|
|
313
|
-
case 'data':
|
|
314
|
-
case 'other':
|
|
315
|
-
this.logs.push({
|
|
316
|
-
transactionIndex: ins.transactionIndex,
|
|
317
|
-
logIndex: this.messagePos,
|
|
318
|
-
instructionAddress: ins.instructionAddress,
|
|
319
|
-
programId: ins.programId,
|
|
320
|
-
kind: msg.kind,
|
|
321
|
-
message: msg.message
|
|
322
|
-
})
|
|
323
|
-
break
|
|
324
|
-
case 'cu':
|
|
325
|
-
if (ins.programId != msg.programId) {
|
|
326
|
-
throw this.error(true, pos, 'unexpected programId in compute unit message')
|
|
327
|
-
}
|
|
328
|
-
ins.computeUnitsConsumed = msg.consumed
|
|
329
|
-
break
|
|
330
|
-
case 'invoke':
|
|
331
|
-
case 'invoke-result':
|
|
332
|
-
return msg
|
|
333
|
-
case 'truncate':
|
|
334
|
-
this.messagesTruncated = true
|
|
335
|
-
this.messagePos += 1
|
|
336
|
-
return msg
|
|
337
|
-
default:
|
|
338
|
-
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
|
|
339
94
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
+
)
|
|
355
110
|
} else {
|
|
356
|
-
throw
|
|
357
|
-
}
|
|
358
|
-
})
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
private logLessTraversal(
|
|
362
|
-
parentStackHeight: number,
|
|
363
|
-
instructions: rpc.Instruction[],
|
|
364
|
-
pos: number,
|
|
365
|
-
cb?: (ins: Instruction, pos: number) => void
|
|
366
|
-
): number {
|
|
367
|
-
while (pos < instructions.length) {
|
|
368
|
-
let instruction = instructions[pos]
|
|
369
|
-
let stackHeight = instruction.stackHeight ?? 2
|
|
370
|
-
if (stackHeight > parentStackHeight) {
|
|
371
|
-
let ins = this.push(stackHeight, instruction)
|
|
372
|
-
cb?.(ins, pos)
|
|
373
|
-
pos += 1
|
|
374
|
-
} else {
|
|
375
|
-
return pos
|
|
111
|
+
throw err
|
|
376
112
|
}
|
|
377
113
|
}
|
|
378
|
-
return pos
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
private push(stackHeight: number, src: rpc.Instruction): Instruction {
|
|
382
|
-
assert(stackHeight > 0)
|
|
383
114
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
115
|
+
this.transactions.push(mapped)
|
|
116
|
+
this.balances.push(...mapBalances(ctx))
|
|
117
|
+
this.tokenBalances.push(...mapTokenBalances(ctx))
|
|
118
|
+
}
|
|
387
119
|
|
|
388
|
-
|
|
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]
|
|
389
123
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if (address.length === stackHeight) {
|
|
395
|
-
address[stackHeight - 1] += 1
|
|
396
|
-
} else {
|
|
397
|
-
assert(address.length + 1 == stackHeight)
|
|
398
|
-
address[stackHeight - 1] = 0
|
|
399
|
-
}
|
|
124
|
+
let inner = ctx.tx.meta.innerInstructions?.flatMap(pack => {
|
|
125
|
+
return pack.index === i ? pack.instructions : []
|
|
126
|
+
})
|
|
400
127
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
128
|
+
new InstructionTreeTraversal(
|
|
129
|
+
ctx,
|
|
130
|
+
messages,
|
|
131
|
+
i,
|
|
132
|
+
ins,
|
|
133
|
+
inner ?? [],
|
|
134
|
+
this.instructions,
|
|
135
|
+
this.logs
|
|
136
|
+
)
|
|
409
137
|
}
|
|
410
|
-
|
|
411
|
-
this.instructions.push(i)
|
|
412
|
-
this.lastAddress = address
|
|
413
|
-
return i
|
|
414
138
|
}
|
|
139
|
+
}
|
|
415
140
|
|
|
416
|
-
private assert(ok: unknown, withMessagePos: boolean, innerPos: number, msg: string): asserts ok {
|
|
417
|
-
if (!ok) throw this.error(withMessagePos, innerPos, msg)
|
|
418
|
-
}
|
|
419
141
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
|
431
158
|
}
|
|
432
159
|
}
|
|
433
160
|
|
|
434
161
|
|
|
435
|
-
function mapBalances(
|
|
436
|
-
getAccount: (idx: number) => Base58Bytes,
|
|
437
|
-
transactionIndex: number,
|
|
438
|
-
tx: rpc.Transaction,
|
|
439
|
-
): Balance[] {
|
|
162
|
+
function mapBalances(ctx: TransactionContext): Balance[] {
|
|
440
163
|
let balances: Balance[] = []
|
|
441
164
|
|
|
442
|
-
let pre = tx.meta.preBalances
|
|
443
|
-
let post = tx.meta.postBalances
|
|
165
|
+
let pre = ctx.tx.meta.preBalances
|
|
166
|
+
let post = ctx.tx.meta.postBalances
|
|
444
167
|
|
|
445
168
|
assert(pre.length == post.length)
|
|
446
169
|
|
|
@@ -449,8 +172,8 @@ function mapBalances(
|
|
|
449
172
|
// nothing changed, don't create an entry
|
|
450
173
|
} else {
|
|
451
174
|
balances.push({
|
|
452
|
-
transactionIndex,
|
|
453
|
-
account: getAccount(i),
|
|
175
|
+
transactionIndex: ctx.transactionIndex,
|
|
176
|
+
account: ctx.getAccount(i),
|
|
454
177
|
pre: BigInt(pre[i]),
|
|
455
178
|
post: BigInt(post[i])
|
|
456
179
|
})
|
|
@@ -467,26 +190,22 @@ function mapBalances(
|
|
|
467
190
|
}
|
|
468
191
|
|
|
469
192
|
|
|
470
|
-
function mapTokenBalances(
|
|
471
|
-
getAccount: (idx: number) => Base58Bytes,
|
|
472
|
-
transactionIndex: number,
|
|
473
|
-
tx: rpc.Transaction
|
|
474
|
-
): TokenBalance[] {
|
|
193
|
+
function mapTokenBalances(ctx: TransactionContext): TokenBalance[] {
|
|
475
194
|
let balances: TokenBalance[] = []
|
|
476
195
|
|
|
477
196
|
let preBalances = new Map(
|
|
478
|
-
tx.meta.preTokenBalances?.map(b => [getAccount(b.accountIndex), b])
|
|
197
|
+
ctx.tx.meta.preTokenBalances?.map(b => [ctx.getAccount(b.accountIndex), b])
|
|
479
198
|
)
|
|
480
199
|
|
|
481
200
|
let postBalances = new Map(
|
|
482
|
-
tx.meta.postTokenBalances?.map(b => [getAccount(b.accountIndex), b])
|
|
201
|
+
ctx.tx.meta.postTokenBalances?.map(b => [ctx.getAccount(b.accountIndex), b])
|
|
483
202
|
)
|
|
484
203
|
|
|
485
204
|
for (let [account, post] of postBalances.entries()) {
|
|
486
205
|
let pre = preBalances.get(account)
|
|
487
206
|
if (pre) {
|
|
488
207
|
balances.push({
|
|
489
|
-
transactionIndex,
|
|
208
|
+
transactionIndex: ctx.transactionIndex,
|
|
490
209
|
account,
|
|
491
210
|
|
|
492
211
|
preProgramId: pre.programId ?? undefined,
|
|
@@ -503,7 +222,7 @@ function mapTokenBalances(
|
|
|
503
222
|
})
|
|
504
223
|
} else {
|
|
505
224
|
balances.push({
|
|
506
|
-
transactionIndex,
|
|
225
|
+
transactionIndex: ctx.transactionIndex,
|
|
507
226
|
account,
|
|
508
227
|
postProgramId: post.programId ?? undefined,
|
|
509
228
|
postMint: post.mint,
|
|
@@ -517,7 +236,7 @@ function mapTokenBalances(
|
|
|
517
236
|
for (let [account, pre] of preBalances.entries()) {
|
|
518
237
|
if (postBalances.has(account)) continue
|
|
519
238
|
balances.push({
|
|
520
|
-
transactionIndex,
|
|
239
|
+
transactionIndex: ctx.transactionIndex,
|
|
521
240
|
account,
|
|
522
241
|
preProgramId: pre.programId ?? undefined,
|
|
523
242
|
preMint: pre.mint,
|
|
@@ -534,8 +253,4 @@ function mapTokenBalances(
|
|
|
534
253
|
})
|
|
535
254
|
|
|
536
255
|
return balances
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
function isIntructionError(err: unknown): err is {InstructionError: [number, string]} {
|
|
540
|
-
return typeof err == 'object' && err != null && 'InstructionError' in err
|
|
541
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
|
+
}
|