@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.
@@ -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
+ }