@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
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import * as rpc from '@subsquid/solana-rpc-data'
|
|
2
|
+
import {unexpectedCase} from '@subsquid/util-internal'
|
|
3
|
+
import assert from 'assert'
|
|
4
|
+
import {Instruction, LogMessage} from './data'
|
|
5
|
+
import {Message, parseLogMessage} from './log-parser'
|
|
6
|
+
import {TransactionContext} from './transaction-context'
|
|
7
|
+
|
|
8
|
+
const PROGRAMS_MISSING_INVOKE_LOG = new Set([
|
|
9
|
+
'AddressLookupTab1e1111111111111111111111111',
|
|
10
|
+
'BPFLoader1111111111111111111111111111111111',
|
|
11
|
+
'BPFLoader2111111111111111111111111111111111',
|
|
12
|
+
'BPFLoaderUpgradeab1e11111111111111111111111',
|
|
13
|
+
'Ed25519SigVerify111111111111111111111111111',
|
|
14
|
+
'KeccakSecp256k11111111111111111111111111111',
|
|
15
|
+
'NativeLoader1111111111111111111111111111111',
|
|
16
|
+
'ZkTokenProof1111111111111111111111111111111',
|
|
17
|
+
'Secp256r1SigVerify1111111111111111111111111',
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
export class ParsingError {
|
|
21
|
+
instructionIndex?: number
|
|
22
|
+
innerInstructionIndex?: number
|
|
23
|
+
programId?: string
|
|
24
|
+
logMessageIndex?: number
|
|
25
|
+
|
|
26
|
+
constructor(public msg: string) {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class MessageStream {
|
|
30
|
+
private messages: Message[]
|
|
31
|
+
private pos = 0
|
|
32
|
+
|
|
33
|
+
constructor(messages: string[]) {
|
|
34
|
+
this.messages = messages.map(parseLogMessage)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get unfinished(): boolean {
|
|
38
|
+
return !this.ended
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get ended(): boolean {
|
|
42
|
+
return this.pos >= this.messages.length || this.messages[this.pos].kind == 'truncate'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get truncated(): boolean {
|
|
46
|
+
return this.pos < this.messages.length && this.messages[this.pos].kind == 'truncate'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get current(): Message {
|
|
50
|
+
assert(this.pos < this.messages.length, 'eof reached')
|
|
51
|
+
return this.messages[this.pos]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get position(): number {
|
|
55
|
+
return this.pos
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
advance(): boolean {
|
|
59
|
+
if (this.truncated) return false
|
|
60
|
+
this.pos = Math.min(this.pos + 1, this.messages.length)
|
|
61
|
+
return this.pos < this.messages.length
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class InstructionTreeTraversal {
|
|
66
|
+
private lastAddress: number[]
|
|
67
|
+
private instructions: rpc.Instruction[]
|
|
68
|
+
private pos = 0
|
|
69
|
+
|
|
70
|
+
constructor(
|
|
71
|
+
private tx: TransactionContext,
|
|
72
|
+
private messages: MessageStream,
|
|
73
|
+
private instructionIndex: number,
|
|
74
|
+
instruction: rpc.Instruction,
|
|
75
|
+
inner: rpc.Instruction[],
|
|
76
|
+
private output: Instruction[],
|
|
77
|
+
private log: LogMessage[]
|
|
78
|
+
) {
|
|
79
|
+
this.lastAddress = [instructionIndex - 1]
|
|
80
|
+
this.instructions = [{...instruction, stackHeight: 1}, ...inner]
|
|
81
|
+
if (this.tx.erroredInstruction >= this.instructionIndex) {
|
|
82
|
+
this.call(1)
|
|
83
|
+
this.finishLogLess()
|
|
84
|
+
} else {
|
|
85
|
+
this.assert(this.instructions.length === 1, 'failed instructions should not have inner calls', 0)
|
|
86
|
+
this.push(1)
|
|
87
|
+
}
|
|
88
|
+
this.assert(this.ended, 'not all inner instructions where consumed', 0)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private call(stackHeight: number): void {
|
|
92
|
+
let ins = this.current
|
|
93
|
+
|
|
94
|
+
this.assert(ins.stackHeight == null || ins.stackHeight === stackHeight, 'stack height mismatch', this.pos)
|
|
95
|
+
|
|
96
|
+
let programId = this.tx.getAccount(ins.programIdIndex)
|
|
97
|
+
|
|
98
|
+
if (
|
|
99
|
+
this.messages.unfinished &&
|
|
100
|
+
this.messages.current.kind === 'invoke' &&
|
|
101
|
+
this.messages.current.programId === programId
|
|
102
|
+
) {
|
|
103
|
+
this.assert(
|
|
104
|
+
this.messages.current.stackHeight === stackHeight,
|
|
105
|
+
'invoke message has unexpected stack height',
|
|
106
|
+
this.pos,
|
|
107
|
+
this.messages.position
|
|
108
|
+
)
|
|
109
|
+
this.messages.advance()
|
|
110
|
+
this.invoke(stackHeight)
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (this.messages.truncated) {
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (PROGRAMS_MISSING_INVOKE_LOG.has(programId)) {
|
|
119
|
+
} else if (
|
|
120
|
+
this.tx.couldFailBeforeInvokeMessage &&
|
|
121
|
+
this.tx.erroredInstruction == this.instructionIndex &&
|
|
122
|
+
this.pos + 1 == this.instructions.length
|
|
123
|
+
) {
|
|
124
|
+
// instruction processing has not reached 'invoke message' logging point
|
|
125
|
+
} else if (this.messages.ended) {
|
|
126
|
+
this.warn('unexpected end of message log', this.pos)
|
|
127
|
+
} else {
|
|
128
|
+
this.warn('missing invoke message', this.pos, this.messages.position)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.push(stackHeight).hasDroppedLogMessages = true
|
|
132
|
+
this.dropNonInvokeMessages()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private invoke(stackHeight: number): void {
|
|
136
|
+
let pos = this.pos
|
|
137
|
+
let ins = this.push(stackHeight)
|
|
138
|
+
|
|
139
|
+
this.takeInstructionMessages(ins, pos)
|
|
140
|
+
|
|
141
|
+
if (this.messages.truncated) {
|
|
142
|
+
ins.hasDroppedLogMessages = true
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (this.messages.ended) {
|
|
147
|
+
throw this.error('unexpected end of log', pos)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let result = this.messages.current
|
|
151
|
+
assert(result.kind === 'invoke-result')
|
|
152
|
+
this.assert(
|
|
153
|
+
result.programId === ins.programId,
|
|
154
|
+
"invoke result message and instruction program ids don't match",
|
|
155
|
+
pos
|
|
156
|
+
)
|
|
157
|
+
ins.error = result.error
|
|
158
|
+
this.messages.advance()
|
|
159
|
+
|
|
160
|
+
// consume invoke-less subcalls,
|
|
161
|
+
// that might have left unvisited due to missing 'invoke' messages
|
|
162
|
+
this.eatInvokeLessSubCalls(ins)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private eatInvokeLessSubCalls(parent: Instruction) {
|
|
166
|
+
while (this.unfinished) {
|
|
167
|
+
let stackHeight: number
|
|
168
|
+
if (this.current.stackHeight == null) {
|
|
169
|
+
if (parent.error) {
|
|
170
|
+
// all remaining calls must belong to the given parent
|
|
171
|
+
stackHeight = parent.instructionAddress.length + 1
|
|
172
|
+
} else {
|
|
173
|
+
stackHeight = 2
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
stackHeight = this.current.stackHeight
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (stackHeight <= parent.instructionAddress.length) return
|
|
180
|
+
|
|
181
|
+
let pos = this.pos
|
|
182
|
+
let ins = this.push(stackHeight)
|
|
183
|
+
|
|
184
|
+
// even if we have some messages emitted,
|
|
185
|
+
// we already assigned them to parent call
|
|
186
|
+
ins.hasDroppedLogMessages = true
|
|
187
|
+
|
|
188
|
+
if (PROGRAMS_MISSING_INVOKE_LOG.has(ins.programId)) {
|
|
189
|
+
// all good, it is expected to not have 'invoke' message
|
|
190
|
+
} else if (
|
|
191
|
+
this.tx.couldFailBeforeInvokeMessage &&
|
|
192
|
+
this.tx.erroredInstruction == this.instructionIndex &&
|
|
193
|
+
this.ended
|
|
194
|
+
) {
|
|
195
|
+
// instruction processing has not reached 'invoke message' logging point
|
|
196
|
+
} else {
|
|
197
|
+
this.warn('missing invoke message for inner instruction', pos)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private takeInstructionMessages(ins: Instruction, pos: number): void {
|
|
203
|
+
while (this.messages.unfinished) {
|
|
204
|
+
let msg = this.messages.current
|
|
205
|
+
switch (msg.kind) {
|
|
206
|
+
case 'log':
|
|
207
|
+
case 'data':
|
|
208
|
+
case 'other':
|
|
209
|
+
this.log.push({
|
|
210
|
+
transactionIndex: this.tx.transactionIndex,
|
|
211
|
+
logIndex: this.messages.position,
|
|
212
|
+
instructionAddress: ins.instructionAddress,
|
|
213
|
+
programId: ins.programId,
|
|
214
|
+
kind: msg.kind,
|
|
215
|
+
message: msg.message,
|
|
216
|
+
})
|
|
217
|
+
this.messages.advance()
|
|
218
|
+
break
|
|
219
|
+
case 'cu':
|
|
220
|
+
if (ins.programId == msg.programId) {
|
|
221
|
+
ins.computeUnitsConsumed = msg.consumed
|
|
222
|
+
this.messages.advance()
|
|
223
|
+
} else {
|
|
224
|
+
throw this.error('unexpected programId in compute unit message', pos, this.messages.position)
|
|
225
|
+
}
|
|
226
|
+
break
|
|
227
|
+
case 'invoke':
|
|
228
|
+
this.call(ins.instructionAddress.length + 1)
|
|
229
|
+
break
|
|
230
|
+
case 'invoke-result':
|
|
231
|
+
case 'truncate':
|
|
232
|
+
return
|
|
233
|
+
default:
|
|
234
|
+
throw unexpectedCase()
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private dropNonInvokeMessages(): void {
|
|
240
|
+
while (this.messages.unfinished) {
|
|
241
|
+
let msg = this.messages.current
|
|
242
|
+
switch (msg.kind) {
|
|
243
|
+
case 'log':
|
|
244
|
+
case 'data':
|
|
245
|
+
case 'cu':
|
|
246
|
+
case 'other':
|
|
247
|
+
this.messages.advance()
|
|
248
|
+
break
|
|
249
|
+
case 'invoke':
|
|
250
|
+
case 'invoke-result':
|
|
251
|
+
case 'truncate':
|
|
252
|
+
return
|
|
253
|
+
default:
|
|
254
|
+
throw unexpectedCase()
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private finishLogLess(): void {
|
|
260
|
+
while (this.unfinished) {
|
|
261
|
+
this.push(this.current.stackHeight ?? 2).hasDroppedLogMessages = true
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private get current(): rpc.Instruction {
|
|
266
|
+
assert(this.pos < this.instructions.length)
|
|
267
|
+
return this.instructions[this.pos]
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private get unfinished(): boolean {
|
|
271
|
+
return this.pos < this.instructions.length
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private get ended(): boolean {
|
|
275
|
+
return !this.unfinished
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private assert(ok: any, msg: string, pos: number, messagePos?: number): void {
|
|
279
|
+
if (!ok) throw this.error(msg, pos, messagePos)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private error(msg: string, pos: number, messagePos?: number): ParsingError {
|
|
283
|
+
let err = new ParsingError(msg)
|
|
284
|
+
err.instructionIndex = this.instructionIndex
|
|
285
|
+
if (pos) {
|
|
286
|
+
err.innerInstructionIndex = pos - 1
|
|
287
|
+
}
|
|
288
|
+
err.programId = this.tx.getAccount(this.instructions[pos].programIdIndex)
|
|
289
|
+
if (messagePos != null) {
|
|
290
|
+
err.logMessageIndex = messagePos
|
|
291
|
+
}
|
|
292
|
+
return err
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private warn(msg: string, pos: number, messagePos?: number): void {
|
|
296
|
+
let {msg: message, ...props} = this.error(msg, pos, messagePos)
|
|
297
|
+
this.tx.warn(props, message)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private push(stackHeight: number): Instruction {
|
|
301
|
+
assert(stackHeight > 0)
|
|
302
|
+
|
|
303
|
+
let ins = this.current
|
|
304
|
+
if (ins.stackHeight != null) {
|
|
305
|
+
assert(stackHeight === ins.stackHeight)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let address = this.lastAddress.slice()
|
|
309
|
+
|
|
310
|
+
while (address.length > stackHeight) {
|
|
311
|
+
address.pop()
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (address.length === stackHeight) {
|
|
315
|
+
address[stackHeight - 1] += 1
|
|
316
|
+
} else {
|
|
317
|
+
assert(address.length + 1 == stackHeight)
|
|
318
|
+
address[stackHeight - 1] = 0
|
|
319
|
+
}
|
|
320
|
+
assert(address[0] === this.instructionIndex)
|
|
321
|
+
|
|
322
|
+
let mapped: Instruction = {
|
|
323
|
+
transactionIndex: this.tx.transactionIndex,
|
|
324
|
+
instructionAddress: address,
|
|
325
|
+
programId: this.tx.getAccount(ins.programIdIndex),
|
|
326
|
+
accounts: ins.accounts.map((a) => this.tx.getAccount(a)),
|
|
327
|
+
data: ins.data,
|
|
328
|
+
isCommitted: this.tx.isCommitted,
|
|
329
|
+
hasDroppedLogMessages: false,
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
this.output.push(mapped)
|
|
333
|
+
this.lastAddress = address
|
|
334
|
+
this.pos = Math.min(this.pos + 1, this.instructions.length)
|
|
335
|
+
return mapped
|
|
336
|
+
}
|
|
337
|
+
}
|