@stellar-expert/tx-meta-effects-parser 7.0.0-rc.8 → 7.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.
@@ -1,302 +1,324 @@
1
- const {StrKey, encodeMuxedAccount, encodeMuxedAccountToAddress} = require('@stellar/stellar-base')
2
- const effectTypes = require('../effect-types')
3
- const {xdrParseScVal, xdrParseAsset, isContractAddress} = require('../parser/tx-xdr-parser-utils')
4
- const {mapSacContract} = require('./sac-contract-mapper')
5
-
6
- const EVENT_TYPES = {
7
- SYSTEM: 0,
8
- CONTRACT: 1,
9
- DIAGNOSTIC: 2
10
- }
11
-
12
- class EventsAnalyzer {
13
- /**
14
- * @param {EffectsAnalyzer} effectsAnalyzer
15
- */
16
- constructor(effectsAnalyzer) {
17
- this.effectsAnalyzer = effectsAnalyzer
18
- this.callStack = []
19
- }
20
-
21
- /**
22
- * @type {[]}
23
- * @private
24
- */
25
- callStack
26
-
27
- analyze() {
28
- this.analyzeDiagnosticEvents()
29
- this.analyzeEvents()
30
- }
31
-
32
- /**
33
- * @private
34
- */
35
- analyzeEvents() {
36
- const {events} = this.effectsAnalyzer
37
- if (!events)
38
- return
39
- //contract-generated events
40
- for (const evt of events) {
41
- const body = evt.body().value()
42
- const rawTopics = body.topics()
43
- const topics = rawTopics.map(xdrParseScVal)
44
- if (topics[0] === 'DATA' && topics[1] === 'set')
45
- continue //skip data entries modifications
46
- const rawData = body.data()
47
- //add event to the pipeline
48
- this.effectsAnalyzer.addEffect({
49
- type: effectTypes.contractEvent,
50
- contract: StrKey.encodeContract(evt.contractId()),
51
- topics,
52
- rawTopics: rawTopics.map(v => v.toXDR('base64')),
53
- data: processEventBodyValue(rawData),
54
- rawData: rawData.toXDR('base64')
55
- })
56
- }
57
- }
58
-
59
-
60
- /**
61
- * @private
62
- */
63
- analyzeDiagnosticEvents() {
64
- const {diagnosticEvents, processSystemEvents, processMetrics, processFailedOpEffects} = this.effectsAnalyzer
65
- if (!diagnosticEvents)
66
- return
67
- const opContractId = this.effectsAnalyzer.retrieveOpContractId()
68
- //diagnostic events
69
- for (const evt of diagnosticEvents) {
70
- if (!processSystemEvents && !(processFailedOpEffects || evt.inSuccessfulContractCall()))
71
- continue //throw new UnexpectedTxMetaChangeError({type: 'diagnostic_event', action: 'failed'})
72
- //parse event
73
- const event = evt.event()
74
- let contractId = event.contractId() || opContractId //contract id may be attached to the event itself, otherwise use contract from operation
75
- if (contractId && typeof contractId !== 'string') {
76
- contractId = StrKey.encodeContract(contractId)
77
- }
78
- this.processDiagnosticEvent(event._attributes.body._value, event._attributes.type.value, contractId, processMetrics)
79
- }
80
- }
81
-
82
- /**
83
- * @param {xdr.ContractEventV0} body
84
- * @param {Number} type
85
- * @param {String} contract
86
- * @param {Boolean} processMetrics
87
- * @private
88
- */
89
- processDiagnosticEvent(body, type, contract, processMetrics) {
90
- const topics = body.topics()
91
- if (!topics?.length)
92
- return
93
- switch (xdrParseScVal(topics[0])) {
94
- case 'fn_call': // contract call
95
- if (type !== EVENT_TYPES.DIAGNOSTIC)
96
- return // skip non-diagnostic events
97
- const rawArgs = body.data()
98
- const funcCall = {
99
- type: effectTypes.contractInvoked,
100
- contract: xdrParseScVal(topics[1], true),
101
- function: xdrParseScVal(topics[2]),
102
- args: processEventBodyValue(rawArgs),
103
- rawArgs: rawArgs.toXDR('base64')
104
- }
105
- //add the invocation to the call stack
106
- if (this.callStack.length) {
107
- funcCall.depth = this.callStack.length
108
- }
109
- this.callStack.push(funcCall)
110
- this.effectsAnalyzer.addEffect(funcCall)
111
- break
112
- case 'fn_return':
113
- if (type !== EVENT_TYPES.DIAGNOSTIC)
114
- return // skip non-diagnostic events
115
- //attach execution result to the contract invocation event
116
- const lastFuncCall = this.callStack.pop()
117
- const result = body.data()
118
- if (result.switch().name !== 'scvVoid') {
119
- lastFuncCall.result = result.toXDR('base64')
120
- }
121
- break
122
- case 'error':
123
- if (type !== EVENT_TYPES.DIAGNOSTIC)
124
- return // skip non-diagnostic events
125
- let code = topics[1].value().value()
126
- if (code.name) {
127
- code = code.name
128
- }
129
- this.effectsAnalyzer.addEffect({
130
- type: effectTypes.contractError,
131
- contract,
132
- code,
133
- details: processEventBodyValue(body.data())
134
- })
135
- break
136
- case 'core_metrics':
137
- if (type !== EVENT_TYPES.DIAGNOSTIC)
138
- return // skip non-diagnostic events
139
- if (!processMetrics)
140
- return
141
- this.effectsAnalyzer.addMetric(contract, xdrParseScVal(topics[1]), parseInt(processEventBodyValue(body.data())))
142
- break
143
- //handle standard token contract events
144
- //see https://github.com/stellar/rs-soroban-sdk/blob/main/soroban-sdk/src/token.rs
145
- case 'transfer': {
146
- if (!matchEventTopicsShape(topics, ['address', 'address', 'str?']))
147
- return
148
- const from = xdrParseScVal(topics[1])
149
- const receiver = xdrParseScVal(topics[2])
150
- let to = receiver
151
- let amount = processEventBodyValue(body.data())
152
- if (amount.amount !== undefined) {
153
- if (amount.to_muxed_id && !to.startsWith('M')) {
154
- to = encodeMuxedAccountToAddress(encodeMuxedAccount(to, amount.to_muxed_id))
155
- amount = amount.amount
156
- }
157
- }
158
- if (to === from) //self transfer - nothing happens
159
- return // TODO: need additional checks
160
- let classicAsset
161
- if (topics.length > 3) {
162
- classicAsset = xdrParseAsset(xdrParseScVal(topics[3]))
163
- if (!mapSacContract(this.effectsAnalyzer, contract, classicAsset)) {
164
- classicAsset = null //not an SAC event
165
- }
166
- }
167
- if (classicAsset) {
168
- if (classicAsset.includes(from)) { //SAC transfer by asset issuer
169
- this.effectsAnalyzer.mint(classicAsset, amount)
170
- }
171
- if (isContractAddress(from)) {
172
- this.effectsAnalyzer.debit(amount, classicAsset, from)
173
- }
174
- if (isContractAddress(to)) {
175
- this.effectsAnalyzer.credit(amount, classicAsset, to)
176
- }
177
- if (classicAsset.includes(receiver)) { //SAC transfer by asset issuer
178
- this.effectsAnalyzer.burn(classicAsset, amount)
179
- }
180
- } else { //other cases
181
- this.effectsAnalyzer.debit(amount, this.effectsAnalyzer.resolveAsset(contract), from)
182
- this.effectsAnalyzer.credit(amount, this.effectsAnalyzer.resolveAsset(contract), to)
183
- }
184
- }
185
- break
186
- case 'mint': {
187
- if (!matchEventTopicsShape(topics, ['address', 'address', 'str?']) && !matchEventTopicsShape(topics, ['address', 'str?']))
188
- return //throw new Error('Non-standard event')
189
- let to = xdrParseScVal(topics[topics[2]?._arm === 'address' ? 2 : 1])
190
- let amount = processEventBodyValue(body.data())
191
- if (amount.amount !== undefined) {
192
- if (amount.to_muxed_id && !to.startsWith('M')) {
193
- to = encodeMuxedAccountToAddress(encodeMuxedAccount(to, amount.to_muxed_id))
194
- amount = amount.amount
195
- }
196
- }
197
- const last = topics[topics.length - 1]
198
- if (last._arm === 'str') {
199
- mapSacContract(this.effectsAnalyzer, contract, xdrParseAsset(xdrParseScVal(last)))
200
- }
201
- const asset = this.effectsAnalyzer.resolveAsset(contract)
202
- this.effectsAnalyzer.mint(asset, amount)
203
- if (isContractAddress(asset) || isContractAddress(to)) {
204
- this.effectsAnalyzer.credit(amount, asset, to)
205
- }
206
- }
207
- break
208
- case 'burn': {
209
- if (!matchEventTopicsShape(topics, ['address', 'str?']))
210
- return //throw new Error('Non-standard event')
211
- const from = xdrParseScVal(topics[1])
212
- const amount = processEventBodyValue(body.data())
213
- if (!amount)
214
- return
215
- if (topics.length > 2) {
216
- mapSacContract(this.effectsAnalyzer, contract, xdrParseAsset(xdrParseScVal(topics[2])))
217
- }
218
- const asset = this.effectsAnalyzer.resolveAsset(contract)
219
- if (isContractAddress(asset) || isContractAddress(from)) {
220
- this.effectsAnalyzer.debit(amount, asset, from)
221
- }
222
- this.effectsAnalyzer.burn(asset, amount)
223
- }
224
- break
225
- case 'clawback': {
226
- if (!matchEventTopicsShape(topics, ['address', 'address', 'str?']) && !matchEventTopicsShape(topics, ['address', 'str?']))
227
- return //throw new Error('Non-standard event')
228
- const from = xdrParseScVal(topics[topics[2]?._arm === 'address' ? 2 : 1])
229
- const amount = processEventBodyValue(body.data())
230
- if (topics.length > 3) {
231
- mapSacContract(this.effectsAnalyzer, contract, xdrParseAsset(xdrParseScVal(topics[3])))
232
- }
233
- const asset = this.effectsAnalyzer.resolveAsset(contract)
234
- this.effectsAnalyzer.debit(amount, asset, from)
235
- this.effectsAnalyzer.burn(asset, amount)
236
- }
237
- break
238
- case 'set_admin': {
239
- if (!matchEventTopicsShape(topics, ['address', 'str?']))
240
- return //throw new Error('Non-standard event')
241
- const currentAdmin = xdrParseScVal(topics[1])
242
- const newAdmin = processEventBodyValue(body.data())
243
- if (topics.length > 2) {
244
- mapSacContract(this.effectsAnalyzer, contract, xdrParseAsset(xdrParseScVal(topics[2])))
245
- }
246
- this.effectsAnalyzer.setAdmin(contract, newAdmin)
247
- }
248
- break
249
- /*case 'set_authorized':*/ //TODO: think about processing this effects
250
- /*case 'approve': {
251
- if (!matchEventTopicsShape(topics, ['address', 'address', 'str?']))
252
- throw new Error('Non-standard event')
253
- const from = xdrParseScVal(topics[1])
254
- const spender = xdrParseScVal(topics[2])
255
- if (topics.length > 3) {
256
- mapSacContract(this.effectsAnalyzer, contractId, xdrParseAsset(xdrParseScVal(topics[3])))
257
- }
258
- }
259
- break*/
260
- }
261
- }
262
- }
263
-
264
- /**
265
- * Compare types in the topics array with expected values
266
- * @param {ScVal[]} topics
267
- * @param {string[]} shape
268
- * @return {boolean}
269
- */
270
- function matchEventTopicsShape(topics, shape) {
271
- if (topics.length > shape.length + 1)
272
- return false
273
- //we ignore the first topic because it's an event name
274
- for (let i = 0; i < shape.length; i++) {
275
- let match = shape[i]
276
- let optional = false
277
- if (match.endsWith('?')) {
278
- match = match.substring(0, match.length - 1)
279
- optional = true
280
- }
281
- const topic = topics[i + 1]
282
- if (topic) {
283
- if (topic._arm !== match)
284
- return false
285
- } else if (!optional)
286
- return false
287
- }
288
- return true
289
- }
290
-
291
- /**
292
- * Retrieve event body value
293
- * @param value
294
- */
295
- function processEventBodyValue(value) {
296
- const innerValue = value.value()
297
- if (!innerValue) //scVoid
298
- return null
299
- return xdrParseScVal(value) //other scValue
300
- }
301
-
1
+ const {StrKey, encodeMuxedAccount, encodeMuxedAccountToAddress} = require('@stellar/stellar-base')
2
+ const effectTypes = require('../effect-types')
3
+ const {xdrParseScVal, xdrParseAsset} = require('../parser/tx-xdr-parser-utils')
4
+ const {isContractAddress, validateAmount} = require('../parser/normalization')
5
+ const {mapSacContract} = require('./sac-contract-mapper')
6
+
7
+ const EVENT_TYPES = {
8
+ SYSTEM: 0,
9
+ CONTRACT: 1,
10
+ DIAGNOSTIC: 2
11
+ }
12
+
13
+ class EventsAnalyzer {
14
+ /**
15
+ * @param {EffectsAnalyzer} effectsAnalyzer
16
+ */
17
+ constructor(effectsAnalyzer) {
18
+ this.effectsAnalyzer = effectsAnalyzer
19
+ this.callStack = []
20
+ }
21
+
22
+ /**
23
+ * @type {[]}
24
+ * @private
25
+ */
26
+ callStack
27
+
28
+ analyze() {
29
+ this.analyzeDiagnosticEvents()
30
+ this.analyzeEvents()
31
+ }
32
+
33
+ /**
34
+ * @private
35
+ */
36
+ analyzeEvents() {
37
+ const {events} = this.effectsAnalyzer
38
+ if (!events)
39
+ return
40
+ //contract-generated events
41
+ for (const evt of events) {
42
+ const body = evt.body().value()
43
+ const rawTopics = body.topics()
44
+ const topics = rawTopics.map(xdrParseScVal)
45
+ if (topics[0] === 'DATA' && topics[1] === 'set')
46
+ continue //skip data entries modifications
47
+ const rawData = body.data()
48
+ //add event to the pipeline
49
+ this.effectsAnalyzer.addEffect({
50
+ type: effectTypes.contractEvent,
51
+ contract: StrKey.encodeContract(evt.contractId()),
52
+ topics,
53
+ rawTopics: rawTopics.map(v => v.toXDR('base64')),
54
+ data: processEventBodyValue(rawData),
55
+ rawData: rawData.toXDR('base64')
56
+ })
57
+ }
58
+ }
59
+
60
+
61
+ /**
62
+ * @private
63
+ */
64
+ analyzeDiagnosticEvents() {
65
+ const {diagnosticEvents, processSystemEvents, processMetrics, processFailedOpEffects} = this.effectsAnalyzer
66
+ if (!diagnosticEvents)
67
+ return
68
+ const opContractId = this.effectsAnalyzer.retrieveOpContractId()
69
+ //diagnostic events
70
+ for (const evt of diagnosticEvents) {
71
+ if (!processSystemEvents && !(processFailedOpEffects || evt.inSuccessfulContractCall()))
72
+ continue //throw new UnexpectedTxMetaChangeError({type: 'diagnostic_event', action: 'failed'})
73
+ //parse event
74
+ const event = evt.event()
75
+ let contractId = event.contractId() || opContractId //contract id may be attached to the event itself, otherwise use contract from operation
76
+ if (contractId && typeof contractId !== 'string') {
77
+ contractId = StrKey.encodeContract(contractId)
78
+ }
79
+ this.processDiagnosticEvent(event._attributes.body._value, event._attributes.type.value, contractId, processMetrics)
80
+ }
81
+ }
82
+
83
+ /**
84
+ * @param {xdr.ContractEventV0} body
85
+ * @param {Number} type
86
+ * @param {String} contract
87
+ * @param {Boolean} processMetrics
88
+ * @private
89
+ */
90
+ processDiagnosticEvent(body, type, contract, processMetrics) {
91
+ const topics = body.topics()
92
+ if (!topics?.length)
93
+ return
94
+ switch (xdrParseScVal(topics[0])) {
95
+ case 'fn_call': // contract call
96
+ if (type !== EVENT_TYPES.DIAGNOSTIC)
97
+ return // skip non-diagnostic events
98
+ const rawArgs = body.data()
99
+ const funcCall = {
100
+ type: effectTypes.contractInvoked,
101
+ contract: xdrParseScVal(topics[1], true),
102
+ function: xdrParseScVal(topics[2]),
103
+ args: processEventBodyValue(rawArgs),
104
+ rawArgs: rawArgs.toXDR('base64')
105
+ }
106
+ //add the invocation to the call stack
107
+ if (this.callStack.length) {
108
+ funcCall.depth = this.callStack.length
109
+ }
110
+ this.callStack.push(funcCall)
111
+ this.effectsAnalyzer.addEffect(funcCall)
112
+ break
113
+ case 'fn_return':
114
+ if (type !== EVENT_TYPES.DIAGNOSTIC)
115
+ return // skip non-diagnostic events
116
+ //attach execution result to the contract invocation event
117
+ const lastFuncCall = this.callStack.pop()
118
+ const result = body.data()
119
+ if (result.switch().name !== 'scvVoid') {
120
+ lastFuncCall.result = result.toXDR('base64')
121
+ }
122
+ break
123
+ case 'error':
124
+ if (type !== EVENT_TYPES.DIAGNOSTIC)
125
+ return // skip non-diagnostic events
126
+ let code = topics[1].value().value()
127
+ if (code.name) {
128
+ code = code.name
129
+ }
130
+ this.effectsAnalyzer.addEffect({
131
+ type: effectTypes.contractError,
132
+ contract,
133
+ code,
134
+ details: processEventBodyValue(body.data())
135
+ })
136
+ break
137
+ case 'core_metrics':
138
+ if (type !== EVENT_TYPES.DIAGNOSTIC)
139
+ return // skip non-diagnostic events
140
+ if (!processMetrics)
141
+ return
142
+ this.effectsAnalyzer.addMetric(contract, xdrParseScVal(topics[1]), parseInt(processEventBodyValue(body.data())))
143
+ break
144
+ //handle standard token contract events
145
+ //see https://github.com/stellar/rs-soroban-sdk/blob/main/soroban-sdk/src/token.rs
146
+ case 'transfer': {
147
+ if (!matchEventTopicsShape(topics, ['address', 'address', 'str?']))
148
+ return
149
+ const from = xdrParseScVal(topics[1])
150
+ const receiver = xdrParseScVal(topics[2])
151
+ let to = receiver
152
+ let amount = processEventBodyValue(body.data())
153
+ if (amount?.amount !== undefined) {
154
+ if (amount.to_muxed_id && !to.startsWith('M')) {
155
+ to = encodeMuxedAccountToAddress(encodeMuxedAccount(to, amount.to_muxed_id))
156
+ amount = amount.amount
157
+ }
158
+ }
159
+ if (validateAmount(amount, false) === null)
160
+ return null
161
+ if (to === from) //self transfer - nothing happens
162
+ return // TODO: need additional checks
163
+ const asset = this.getAssetFromEventTopics(topics, contract)
164
+ if (!StrKey.isValidContract(asset)) {
165
+ if (asset.includes(from)) { //SAC transfer by asset issuer
166
+ this.effectsAnalyzer.mint(asset, amount)
167
+ }
168
+ if (isContractAddress(from)) {
169
+ this.effectsAnalyzer.debit(amount, asset, from)
170
+ }
171
+ if (isContractAddress(to)) {
172
+ this.effectsAnalyzer.credit(amount, asset, to)
173
+ }
174
+ if (asset.includes(receiver)) { //SAC transfer by asset issuer
175
+ this.effectsAnalyzer.burn(asset, amount)
176
+ }
177
+ } else { //other cases
178
+ this.effectsAnalyzer.debit(amount, asset, from)
179
+ this.effectsAnalyzer.credit(amount, asset, to)
180
+ }
181
+ }
182
+ break
183
+ case 'mint': {
184
+ if (!matchEventTopicsShape(topics, ['address', 'address', 'str?']) && !matchEventTopicsShape(topics, ['address', 'str?']))
185
+ return //throw new Error('Non-standard event')
186
+ let to = xdrParseScVal(topics[topics[2]?._arm === 'address' ? 2 : 1])
187
+ let amount = processEventBodyValue(body.data())
188
+ if (amount?.amount !== undefined) {
189
+ if (amount.to_muxed_id && !to.startsWith('M')) {
190
+ to = encodeMuxedAccountToAddress(encodeMuxedAccount(to, amount.to_muxed_id))
191
+ amount = amount.amount
192
+ }
193
+ }
194
+ if (validateAmount(amount, false) === null)
195
+ return null
196
+ validateAmount(amount)
197
+ const asset = this.getAssetFromEventTopics(topics, contract)
198
+ this.effectsAnalyzer.mint(asset, amount)
199
+ if (isContractAddress(asset) || isContractAddress(to)) {
200
+ this.effectsAnalyzer.credit(amount, asset, to)
201
+ }
202
+ }
203
+ break
204
+ case 'burn': {
205
+ if (!matchEventTopicsShape(topics, ['address', 'str?']))
206
+ return //throw new Error('Non-standard event')
207
+ const from = xdrParseScVal(topics[1])
208
+ const amount = processEventBodyValue(body.data())
209
+ if (validateAmount(amount, false) === null)
210
+ return null
211
+ const asset = this.getAssetFromEventTopics(topics, contract)
212
+ if (isContractAddress(asset) || isContractAddress(from)) {
213
+ this.effectsAnalyzer.debit(amount, asset, from)
214
+ }
215
+ this.effectsAnalyzer.burn(asset, amount)
216
+ }
217
+ break
218
+ case 'clawback': {
219
+ if (!matchEventTopicsShape(topics, ['address', 'address', 'str?']) && !matchEventTopicsShape(topics, ['address', 'str?']))
220
+ return //throw new Error('Non-standard event')
221
+ const from = xdrParseScVal(topics[topics[2]?._arm === 'address' ? 2 : 1])
222
+ const amount = processEventBodyValue(body.data())
223
+ if (validateAmount(amount, false) === null)
224
+ return null
225
+ const asset = this.getAssetFromEventTopics(topics, contract)
226
+ if (StrKey.isValidContract(from)) { //transfer tokens from account only in case of contract assets to avoid double debits
227
+ this.effectsAnalyzer.debit(amount, asset, from)
228
+ this.effectsAnalyzer.burn(asset, amount)
229
+ }
230
+ }
231
+ break
232
+ case 'set_admin': {
233
+ if (!matchEventTopicsShape(topics, ['address', 'str?']))
234
+ return //throw new Error('Non-standard event')
235
+ const currentAdmin = xdrParseScVal(topics[1])
236
+ const newAdmin = processEventBodyValue(body.data())
237
+ this.getAssetFromEventTopics(topics, contract)
238
+ this.effectsAnalyzer.setAdmin(contract, newAdmin)
239
+ }
240
+ break
241
+ case 'set_authorized': {
242
+ if (!matchEventTopicsShape(topics, ['address', 'str?']))
243
+ return //throw new Error('Non-standard event')
244
+ const trustor = xdrParseScVal(topics[1])
245
+ const asset = this.getAssetFromEventTopics(topics, contract)
246
+ const isAuthorized = processEventBodyValue(body.data())
247
+ this.effectsAnalyzer.addEffect({
248
+ type: effectTypes.trustlineAuthorizationUpdated,
249
+ trustor,
250
+ asset,
251
+ flags: isAuthorized ? 1 : 0,
252
+ prevFlags: isAuthorized ? 0 : 1
253
+ })
254
+ }
255
+ break
256
+ //TODO: think about processing these effects
257
+ /*case 'approve': {
258
+ if (!matchEventTopicsShape(topics, ['address', 'address', 'str?']))
259
+ throw new Error('Non-standard event')
260
+ const from = xdrParseScVal(topics[1])
261
+ const spender = xdrParseScVal(topics[2])
262
+ if (topics.length > 3) {
263
+ mapSacContract(this.effectsAnalyzer, contractId, xdrParseAsset(xdrParseScVal(topics[3])))
264
+ }
265
+ }
266
+ break*/
267
+ }
268
+ }
269
+
270
+ /**
271
+ * @param {ScVal[]} topics
272
+ * @param {string} contract
273
+ * @return {string|null}
274
+ * @private
275
+ */
276
+ getAssetFromEventTopics(topics, contract) {
277
+ const last = topics[topics.length - 1]
278
+ if (last._arm === 'str') {
279
+ const classicAsset = xdrParseAsset(xdrParseScVal(last))
280
+ mapSacContract(this.effectsAnalyzer, contract, classicAsset)
281
+ }
282
+ return this.effectsAnalyzer.resolveAsset(contract)
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Compare types in the topics array with expected values
288
+ * @param {ScVal[]} topics
289
+ * @param {string[]} shape
290
+ * @return {boolean}
291
+ */
292
+ function matchEventTopicsShape(topics, shape) {
293
+ if (topics.length > shape.length + 1)
294
+ return false
295
+ //we ignore the first topic because it's an event name
296
+ for (let i = 0; i < shape.length; i++) {
297
+ let match = shape[i]
298
+ let optional = false
299
+ if (match.endsWith('?')) {
300
+ match = match.substring(0, match.length - 1)
301
+ optional = true
302
+ }
303
+ const topic = topics[i + 1]
304
+ if (topic) {
305
+ if (topic._arm !== match)
306
+ return false
307
+ } else if (!optional)
308
+ return false
309
+ }
310
+ return true
311
+ }
312
+
313
+ /**
314
+ * Retrieve event body value
315
+ * @param value
316
+ */
317
+ function processEventBodyValue(value) {
318
+ const innerValue = value.value()
319
+ if (innerValue === undefined) //scVoid
320
+ return null
321
+ return xdrParseScVal(value) //other scValue
322
+ }
323
+
302
324
  module.exports = EventsAnalyzer