@stellar-expert/tx-meta-effects-parser 7.0.0-rc.19 → 7.0.0-rc.20

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,322 +1,322 @@
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 (typeof amount !== 'string')
159
- return null
160
- if (to === from) //self transfer - nothing happens
161
- return // TODO: need additional checks
162
- const asset = this.getAssetFromEventTopics(topics, contract)
163
- if (!StrKey.isValidContract(asset)) {
164
- if (asset.includes(from)) { //SAC transfer by asset issuer
165
- this.effectsAnalyzer.mint(asset, amount)
166
- }
167
- if (isContractAddress(from)) {
168
- this.effectsAnalyzer.debit(amount, asset, from)
169
- }
170
- if (isContractAddress(to)) {
171
- this.effectsAnalyzer.credit(amount, asset, to)
172
- }
173
- if (asset.includes(receiver)) { //SAC transfer by asset issuer
174
- this.effectsAnalyzer.burn(asset, amount)
175
- }
176
- } else { //other cases
177
- this.effectsAnalyzer.debit(amount, asset, from)
178
- this.effectsAnalyzer.credit(amount, asset, to)
179
- }
180
- }
181
- break
182
- case 'mint': {
183
- if (!matchEventTopicsShape(topics, ['address', 'address', 'str?']) && !matchEventTopicsShape(topics, ['address', 'str?']))
184
- return //throw new Error('Non-standard event')
185
- let to = xdrParseScVal(topics[topics[2]?._arm === 'address' ? 2 : 1])
186
- let amount = processEventBodyValue(body.data())
187
- if (amount?.amount !== undefined) {
188
- if (amount.to_muxed_id && !to.startsWith('M')) {
189
- to = encodeMuxedAccountToAddress(encodeMuxedAccount(to, amount.to_muxed_id))
190
- amount = amount.amount
191
- }
192
- }
193
- if (typeof amount !== 'string')
194
- return null
195
- const asset = this.getAssetFromEventTopics(topics, contract)
196
- this.effectsAnalyzer.mint(asset, amount)
197
- if (isContractAddress(asset) || isContractAddress(to)) {
198
- this.effectsAnalyzer.credit(amount, asset, to)
199
- }
200
- }
201
- break
202
- case 'burn': {
203
- if (!matchEventTopicsShape(topics, ['address', 'str?']))
204
- return //throw new Error('Non-standard event')
205
- const from = xdrParseScVal(topics[1])
206
- const amount = processEventBodyValue(body.data())
207
- if (typeof amount !== 'string')
208
- return null
209
- const asset = this.getAssetFromEventTopics(topics, contract)
210
- if (isContractAddress(asset) || isContractAddress(from)) {
211
- this.effectsAnalyzer.debit(amount, asset, from)
212
- }
213
- this.effectsAnalyzer.burn(asset, amount)
214
- }
215
- break
216
- case 'clawback': {
217
- if (!matchEventTopicsShape(topics, ['address', 'address', 'str?']) && !matchEventTopicsShape(topics, ['address', 'str?']))
218
- return //throw new Error('Non-standard event')
219
- const from = xdrParseScVal(topics[topics[2]?._arm === 'address' ? 2 : 1])
220
- const amount = processEventBodyValue(body.data())
221
- if (typeof amount !== 'string')
222
- return null
223
- const asset = this.getAssetFromEventTopics(topics, contract)
224
- if (StrKey.isValidContract(from)) { //transfer tokens from account only in case of contract assets to avoid double debits
225
- this.effectsAnalyzer.debit(amount, asset, from)
226
- this.effectsAnalyzer.burn(asset, amount)
227
- }
228
- }
229
- break
230
- case 'set_admin': {
231
- if (!matchEventTopicsShape(topics, ['address', 'str?']))
232
- return //throw new Error('Non-standard event')
233
- const currentAdmin = xdrParseScVal(topics[1])
234
- const newAdmin = processEventBodyValue(body.data())
235
- this.getAssetFromEventTopics(topics, contract)
236
- this.effectsAnalyzer.setAdmin(contract, newAdmin)
237
- }
238
- break
239
- case 'set_authorized': {
240
- if (!matchEventTopicsShape(topics, ['address', 'str?']))
241
- return //throw new Error('Non-standard event')
242
- const trustor = xdrParseScVal(topics[1])
243
- const asset = this.getAssetFromEventTopics(topics, contract)
244
- const isAuthorized = processEventBodyValue(body.data())
245
- this.effectsAnalyzer.addEffect({
246
- type: effectTypes.trustlineAuthorizationUpdated,
247
- trustor,
248
- asset,
249
- flags: isAuthorized ? 1 : 0,
250
- prevFlags: isAuthorized ? 0 : 1
251
- })
252
- }
253
- break
254
- //TODO: think about processing these effects
255
- /*case 'approve': {
256
- if (!matchEventTopicsShape(topics, ['address', 'address', 'str?']))
257
- throw new Error('Non-standard event')
258
- const from = xdrParseScVal(topics[1])
259
- const spender = xdrParseScVal(topics[2])
260
- if (topics.length > 3) {
261
- mapSacContract(this.effectsAnalyzer, contractId, xdrParseAsset(xdrParseScVal(topics[3])))
262
- }
263
- }
264
- break*/
265
- }
266
- }
267
-
268
- /**
269
- * @param {ScVal[]} topics
270
- * @param {string} contract
271
- * @return {string|null}
272
- * @private
273
- */
274
- getAssetFromEventTopics(topics, contract) {
275
- const last = topics[topics.length - 1]
276
- if (last._arm === 'str') {
277
- const classicAsset = xdrParseAsset(xdrParseScVal(last))
278
- mapSacContract(this.effectsAnalyzer, contract, classicAsset)
279
- }
280
- return this.effectsAnalyzer.resolveAsset(contract)
281
- }
282
- }
283
-
284
- /**
285
- * Compare types in the topics array with expected values
286
- * @param {ScVal[]} topics
287
- * @param {string[]} shape
288
- * @return {boolean}
289
- */
290
- function matchEventTopicsShape(topics, shape) {
291
- if (topics.length > shape.length + 1)
292
- return false
293
- //we ignore the first topic because it's an event name
294
- for (let i = 0; i < shape.length; i++) {
295
- let match = shape[i]
296
- let optional = false
297
- if (match.endsWith('?')) {
298
- match = match.substring(0, match.length - 1)
299
- optional = true
300
- }
301
- const topic = topics[i + 1]
302
- if (topic) {
303
- if (topic._arm !== match)
304
- return false
305
- } else if (!optional)
306
- return false
307
- }
308
- return true
309
- }
310
-
311
- /**
312
- * Retrieve event body value
313
- * @param value
314
- */
315
- function processEventBodyValue(value) {
316
- const innerValue = value.value()
317
- if (innerValue === undefined) //scVoid
318
- return null
319
- return xdrParseScVal(value) //other scValue
320
- }
321
-
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 (typeof amount !== 'string')
159
+ return null
160
+ if (to === from) //self transfer - nothing happens
161
+ return // TODO: need additional checks
162
+ const asset = this.getAssetFromEventTopics(topics, contract)
163
+ if (!StrKey.isValidContract(asset)) {
164
+ if (asset.includes(from)) { //SAC transfer by asset issuer
165
+ this.effectsAnalyzer.mint(asset, amount)
166
+ }
167
+ if (isContractAddress(from)) {
168
+ this.effectsAnalyzer.debit(amount, asset, from)
169
+ }
170
+ if (isContractAddress(to)) {
171
+ this.effectsAnalyzer.credit(amount, asset, to)
172
+ }
173
+ if (asset.includes(receiver)) { //SAC transfer by asset issuer
174
+ this.effectsAnalyzer.burn(asset, amount)
175
+ }
176
+ } else { //other cases
177
+ this.effectsAnalyzer.debit(amount, asset, from)
178
+ this.effectsAnalyzer.credit(amount, asset, to)
179
+ }
180
+ }
181
+ break
182
+ case 'mint': {
183
+ if (!matchEventTopicsShape(topics, ['address', 'address', 'str?']) && !matchEventTopicsShape(topics, ['address', 'str?']))
184
+ return //throw new Error('Non-standard event')
185
+ let to = xdrParseScVal(topics[topics[2]?._arm === 'address' ? 2 : 1])
186
+ let amount = processEventBodyValue(body.data())
187
+ if (amount?.amount !== undefined) {
188
+ if (amount.to_muxed_id && !to.startsWith('M')) {
189
+ to = encodeMuxedAccountToAddress(encodeMuxedAccount(to, amount.to_muxed_id))
190
+ amount = amount.amount
191
+ }
192
+ }
193
+ if (typeof amount !== 'string')
194
+ return null
195
+ const asset = this.getAssetFromEventTopics(topics, contract)
196
+ this.effectsAnalyzer.mint(asset, amount)
197
+ if (isContractAddress(asset) || isContractAddress(to)) {
198
+ this.effectsAnalyzer.credit(amount, asset, to)
199
+ }
200
+ }
201
+ break
202
+ case 'burn': {
203
+ if (!matchEventTopicsShape(topics, ['address', 'str?']))
204
+ return //throw new Error('Non-standard event')
205
+ const from = xdrParseScVal(topics[1])
206
+ const amount = processEventBodyValue(body.data())
207
+ if (typeof amount !== 'string')
208
+ return null
209
+ const asset = this.getAssetFromEventTopics(topics, contract)
210
+ if (isContractAddress(asset) || isContractAddress(from)) {
211
+ this.effectsAnalyzer.debit(amount, asset, from)
212
+ }
213
+ this.effectsAnalyzer.burn(asset, amount)
214
+ }
215
+ break
216
+ case 'clawback': {
217
+ if (!matchEventTopicsShape(topics, ['address', 'address', 'str?']) && !matchEventTopicsShape(topics, ['address', 'str?']))
218
+ return //throw new Error('Non-standard event')
219
+ const from = xdrParseScVal(topics[topics[2]?._arm === 'address' ? 2 : 1])
220
+ const amount = processEventBodyValue(body.data())
221
+ if (typeof amount !== 'string')
222
+ return null
223
+ const asset = this.getAssetFromEventTopics(topics, contract)
224
+ if (StrKey.isValidContract(from)) { //transfer tokens from account only in case of contract assets to avoid double debits
225
+ this.effectsAnalyzer.debit(amount, asset, from)
226
+ this.effectsAnalyzer.burn(asset, amount)
227
+ }
228
+ }
229
+ break
230
+ case 'set_admin': {
231
+ if (!matchEventTopicsShape(topics, ['address', 'str?']))
232
+ return //throw new Error('Non-standard event')
233
+ const currentAdmin = xdrParseScVal(topics[1])
234
+ const newAdmin = processEventBodyValue(body.data())
235
+ this.getAssetFromEventTopics(topics, contract)
236
+ this.effectsAnalyzer.setAdmin(contract, newAdmin)
237
+ }
238
+ break
239
+ case 'set_authorized': {
240
+ if (!matchEventTopicsShape(topics, ['address', 'str?']))
241
+ return //throw new Error('Non-standard event')
242
+ const trustor = xdrParseScVal(topics[1])
243
+ const asset = this.getAssetFromEventTopics(topics, contract)
244
+ const isAuthorized = processEventBodyValue(body.data())
245
+ this.effectsAnalyzer.addEffect({
246
+ type: effectTypes.trustlineAuthorizationUpdated,
247
+ trustor,
248
+ asset,
249
+ flags: isAuthorized ? 1 : 0,
250
+ prevFlags: isAuthorized ? 0 : 1
251
+ })
252
+ }
253
+ break
254
+ //TODO: think about processing these effects
255
+ /*case 'approve': {
256
+ if (!matchEventTopicsShape(topics, ['address', 'address', 'str?']))
257
+ throw new Error('Non-standard event')
258
+ const from = xdrParseScVal(topics[1])
259
+ const spender = xdrParseScVal(topics[2])
260
+ if (topics.length > 3) {
261
+ mapSacContract(this.effectsAnalyzer, contractId, xdrParseAsset(xdrParseScVal(topics[3])))
262
+ }
263
+ }
264
+ break*/
265
+ }
266
+ }
267
+
268
+ /**
269
+ * @param {ScVal[]} topics
270
+ * @param {string} contract
271
+ * @return {string|null}
272
+ * @private
273
+ */
274
+ getAssetFromEventTopics(topics, contract) {
275
+ const last = topics[topics.length - 1]
276
+ if (last._arm === 'str') {
277
+ const classicAsset = xdrParseAsset(xdrParseScVal(last))
278
+ mapSacContract(this.effectsAnalyzer, contract, classicAsset)
279
+ }
280
+ return this.effectsAnalyzer.resolveAsset(contract)
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Compare types in the topics array with expected values
286
+ * @param {ScVal[]} topics
287
+ * @param {string[]} shape
288
+ * @return {boolean}
289
+ */
290
+ function matchEventTopicsShape(topics, shape) {
291
+ if (topics.length > shape.length + 1)
292
+ return false
293
+ //we ignore the first topic because it's an event name
294
+ for (let i = 0; i < shape.length; i++) {
295
+ let match = shape[i]
296
+ let optional = false
297
+ if (match.endsWith('?')) {
298
+ match = match.substring(0, match.length - 1)
299
+ optional = true
300
+ }
301
+ const topic = topics[i + 1]
302
+ if (topic) {
303
+ if (topic._arm !== match)
304
+ return false
305
+ } else if (!optional)
306
+ return false
307
+ }
308
+ return true
309
+ }
310
+
311
+ /**
312
+ * Retrieve event body value
313
+ * @param value
314
+ */
315
+ function processEventBodyValue(value) {
316
+ const innerValue = value.value()
317
+ if (innerValue === undefined) //scVoid
318
+ return null
319
+ return xdrParseScVal(value) //other scValue
320
+ }
321
+
322
322
  module.exports = EventsAnalyzer