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