@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,1123 +1,1142 @@
1
- const {StrKey, hash, xdr, nativeToScVal} = require('@stellar/stellar-base')
2
- const effectTypes = require('./effect-types')
3
- const {parseLedgerEntryChanges} = require('./parser/ledger-entry-changes-parser')
4
- const {xdrParseAsset, xdrParseAccountAddress, xdrParseScVal} = require('./parser/tx-xdr-parser-utils')
5
- const {analyzeSignerChanges} = require('./aggregation/signer-changes-analyzer')
6
- const {contractIdFromPreimage} = require('./parser/contract-preimage-encoder')
7
- const EventsAnalyzer = require('./aggregation/events-analyzer')
8
- const AssetSupplyAnalyzer = require('./aggregation/asset-supply-analyzer')
9
- const {mapSacContract} = require('./aggregation/sac-contract-mapper')
10
- const {UnexpectedTxMetaChangeError, TxMetaEffectParserError} = require('./errors')
11
- const {generateContractCodeEntryHash} = require('./parser/ledger-key')
12
-
13
- class EffectsAnalyzer {
14
- constructor({
15
- operation,
16
- meta,
17
- result,
18
- network,
19
- events,
20
- diagnosticEvents,
21
- mapSac,
22
- processSystemEvents,
23
- processFailedOpEffects,
24
- processMetrics
25
- }) {
26
- //set execution context
27
- if (!operation.source)
28
- throw new TxMetaEffectParserError('Operation source is not explicitly defined')
29
- this.operation = operation
30
- this.isContractCall = this.operation.type === 'invokeHostFunction'
31
- this.result = result
32
- this.changes = parseLedgerEntryChanges(meta)
33
- this.source = this.operation.source
34
- this.events = events
35
- this.processFailedOpEffects = processFailedOpEffects
36
- this.processMetrics = processMetrics
37
- if (diagnosticEvents?.length) {
38
- this.diagnosticEvents = diagnosticEvents
39
- if (processSystemEvents) {
40
- this.processSystemEvents = true
41
- }
42
- }
43
- this.network = network
44
- if (mapSac) {
45
- this.sacMap = {}
46
- }
47
- }
48
-
49
- /**
50
- * @type {{}[]}
51
- * @internal
52
- * @readonly
53
- */
54
- effects = []
55
- /**
56
- * @type {Object}
57
- * @private
58
- * @readonly
59
- */
60
- operation = null
61
- /**
62
- * @type {String}
63
- * @readonly
64
- */
65
- network
66
- /**
67
- * @type {{}}
68
- * @readonly
69
- */
70
- sacMap
71
- /**
72
- * @type {ParsedLedgerEntryMeta[]}
73
- * @private
74
- * @readonly
75
- */
76
- changes = null
77
- /**
78
- * @type {Object}
79
- * @private
80
- * @readonly
81
- */
82
- result = null
83
- /**
84
- * @type {String}
85
- * @private
86
- * @readonly
87
- */
88
- source = ''
89
- /**
90
- * @type {Boolean}
91
- * @private
92
- */
93
- isContractCall = false
94
- /**
95
- * @type {Boolean}
96
- * @readonly
97
- */
98
- processSystemEvents = false
99
- /**
100
- * @type {Boolean}
101
- * @readonly
102
- */
103
- processMetrics = true
104
- /**
105
- * @type {{}}
106
- * @private
107
- */
108
- metrics
109
-
110
- analyze() {
111
- //find appropriate parser method
112
- const parse = this[this.operation.type]
113
- if (parse) {
114
- parse.call(this)
115
- }
116
- //process Soroban events
117
- new EventsAnalyzer(this).analyze()
118
- //process state data changes in the end
119
- this.processStateChanges()
120
- //process ledger entry changes
121
- this.processChanges()
122
- //handle effects that are processed indirectly
123
- this.processSponsorshipEffects()
124
- //calculate minted/burned assets
125
- new AssetSupplyAnalyzer(this).analyze()
126
- //add Soroban op metrics if available
127
- if (this.metrics) {
128
- this.addEffect(this.metrics)
129
- }
130
- return this.effects
131
- }
132
-
133
- /**
134
- * @param {{}} effect
135
- * @param {Number} [atPosition]
136
- */
137
- addEffect(effect, atPosition) {
138
- if (!effect.source) {
139
- effect.source = this.source
140
- }
141
- if (atPosition !== undefined) {
142
- this.effects.splice(atPosition < 0 ? 0 : atPosition, 0, effect)
143
- } else {
144
- this.effects.push(effect)
145
- }
146
- }
147
-
148
- debit(amount, asset, source, balance) {
149
- if (amount === '0')
150
- return
151
- const effect = {
152
- type: effectTypes.accountDebited,
153
- source,
154
- asset,
155
- amount: validateAmount(amount)
156
- }
157
- if (balance !== undefined) {
158
- effect.balance = balance
159
- }
160
- this.addEffect(effect)
161
- }
162
-
163
- credit(amount, asset, source, balance) {
164
- if (amount === '0')
165
- return
166
- const effect = {
167
- type: effectTypes.accountCredited,
168
- source,
169
- asset,
170
- amount: validateAmount(amount)
171
- }
172
- if (balance !== undefined) {
173
- effect.balance = balance
174
- }
175
- this.addEffect(effect)
176
- }
177
-
178
- mint(asset, amount, autoLookupPosition = false) {
179
- const position = autoLookupPosition ?
180
- this.effects.findIndex(e => e.asset === asset || e.assets?.find(a => a.asset === asset)) :
181
- undefined
182
- this.addEffect({
183
- type: effectTypes.assetMinted,
184
- asset,
185
- amount: validateAmount(amount)
186
- }, position)
187
- }
188
-
189
- burn(asset, amount, position = undefined) {
190
- this.addEffect({
191
- type: effectTypes.assetBurned,
192
- asset,
193
- amount: validateAmount(amount)
194
- }, position)
195
- }
196
-
197
- addMetric(contract, metric, value) {
198
- let {metrics} = this
199
- if (!metrics) {
200
- metrics = this.metrics = {
201
- type: effectTypes.contractMetrics,
202
- contract
203
- }
204
- }
205
- metrics[metric] = value
206
- }
207
-
208
- addFeeMetric(metaValue) {
209
- const {sorobanMeta} = metaValue._attributes
210
- if (!sorobanMeta)
211
- return
212
- const sorobanExt = sorobanMeta._attributes.ext._value
213
- if (sorobanExt) {
214
- const attrs = sorobanExt._attributes
215
- const fee = {
216
- nonrefundable: parseInt(parseLargeInt(attrs.totalNonRefundableResourceFeeCharged)),
217
- refundable: parseInt(parseLargeInt(attrs.totalRefundableResourceFeeCharged)),
218
- rent: parseInt(parseLargeInt(attrs.rentFeeCharged))
219
- }
220
- this.addMetric(this.retrieveOpContractId(), 'fee', fee)
221
- }
222
- }
223
-
224
- setOptions() {
225
- const sourceAccount = normalizeAddress(this.source)
226
- const change = this.changes.find(ch => ch.type === 'account' && ch.before.address === sourceAccount)
227
- if (!change)
228
- return // failed tx or no changes
229
- const {before, after} = change
230
- if (before.homeDomain !== after.homeDomain) {
231
- this.addEffect({
232
- type: effectTypes.accountHomeDomainUpdated,
233
- domain: after.homeDomain
234
- })
235
- }
236
- if (before.thresholds !== after.thresholds) {
237
- this.addEffect({
238
- type: effectTypes.accountThresholdsUpdated,
239
- thresholds: after.thresholds.split(',').map(v => parseInt(v, 10))
240
- })
241
- }
242
- if (before.flags !== after.flags) {
243
- this.addEffect({
244
- type: effectTypes.accountFlagsUpdated,
245
- flags: after.flags,
246
- prevFlags: before.flags
247
- })
248
- }
249
- if (before.inflationDest !== after.inflationDest) {
250
- this.addEffect({
251
- type: effectTypes.accountInflationDestinationUpdated,
252
- inflationDestination: after.inflationDest
253
- })
254
- }
255
- }
256
-
257
- allowTrust() {
258
- this.setTrustLineFlags()
259
- }
260
-
261
- setTrustLineFlags() {
262
- if (!this.changes.length)
263
- return
264
- const trustAsset = xdrParseAsset(this.operation.asset || {code: this.operation.assetCode, issuer: normalizeAddress(this.source)})
265
- const change = this.changes.find(ch => ch.type === 'trustline' && ch.before.asset === trustAsset)
266
- if (!change)
267
- return
268
- if (change.action !== 'updated')
269
- throw new UnexpectedTxMetaChangeError(change)
270
- const {before, after} = change
271
- if (before.flags !== after.flags) {
272
- this.addEffect({
273
- type: effectTypes.trustlineAuthorizationUpdated,
274
- trustor: this.operation.trustor,
275
- asset: after.asset,
276
- flags: after.flags,
277
- prevFlags: before.flags
278
- })
279
- for (const change of this.changes) {
280
- if (change.type !== 'liquidityPool')
281
- continue
282
- const {before, after} = change
283
- this.addEffect({
284
- type: effectTypes.liquidityPoolWithdrew,
285
- source: this.operation.trustor,
286
- pool: before.pool,
287
- assets: before.asset.map((asset, i) => ({
288
- asset,
289
- amount: (BigInt(before.amount[i]) - (after ? BigInt(after.amount[i]) : 0n)).toString()
290
- })),
291
- shares: (BigInt(before.shares) - (after ? BigInt(after.shares) : 0n)).toString()
292
- })
293
- }
294
- }
295
- }
296
-
297
- inflation() {
298
- /*const paymentEffects = (result.inflationPayouts || []).map(ip => ({
299
- type: effectTypes.accountCredited,
300
- source: ip.account,
301
- asset: 'XLM',
302
- amount: ip.amount
303
- }))*/
304
- this.addEffect({type: effectTypes.inflation})
305
- }
306
-
307
- bumpSequence() {
308
- if (!this.changes.length)
309
- return
310
- const change = this.changes.find(ch => ch.type === 'account')
311
- if (!change)
312
- return //failed tx or no changes
313
- const {before, after} = change
314
- if (before.sequence !== after.sequence) {
315
- this.addEffect({
316
- type: effectTypes.sequenceBumped,
317
- sequence: after.sequence
318
- })
319
- }
320
- }
321
-
322
- pathPaymentStrictReceive() {
323
- this.processDexOperationEffects()
324
- }
325
-
326
- pathPaymentStrictSend() {
327
- this.processDexOperationEffects()
328
- }
329
-
330
- manageSellOffer() {
331
- this.processDexOperationEffects()
332
- }
333
-
334
- manageBuyOffer() {
335
- this.processDexOperationEffects()
336
- }
337
-
338
- createPassiveSellOffer() {
339
- this.processDexOperationEffects()
340
- }
341
-
342
- liquidityPoolDeposit() {
343
- const {liquidityPoolId} = this.operation
344
- const change = this.changes.find(ch => ch.type === 'liquidityPool' && ch.action === 'updated' && ch.after.pool === liquidityPoolId)
345
- if (!change) //tx failed
346
- return
347
- const {before, after} = change
348
- this.addEffect({
349
- type: effectTypes.liquidityPoolDeposited,
350
- pool: this.operation.liquidityPoolId,
351
- assets: after.asset.map((asset, i) => ({
352
- asset,
353
- amount: (after.amount[i] - before.amount[i]).toString()
354
- })),
355
- shares: (after.shares - before.shares).toString()
356
- })
357
- }
358
-
359
- liquidityPoolWithdraw() {
360
- const pool = this.operation.liquidityPoolId
361
- const change = this.changes.find(ch => ch.type === 'liquidityPool' && ch.action === 'updated' && ch.before.pool === pool)
362
- if (!change) //tx failed
363
- return
364
- const {before, after} = change
365
- this.addEffect({
366
- type: effectTypes.liquidityPoolWithdrew,
367
- pool,
368
- assets: before.asset.map((asset, i) => ({
369
- asset,
370
- amount: (before.amount[i] - after.amount[i]).toString()
371
- })),
372
- shares: (before.shares - after.shares).toString()
373
- })
374
- }
375
-
376
- invokeHostFunction() {
377
- const {func} = this.operation
378
- const value = func.value()
379
- switch (func.arm()) {
380
- case 'invokeContract':
381
- if (!this.diagnosticEvents) {
382
- //add top-level contract invocation effect only if diagnostic events are unavailable
383
- const rawArgs = value.args()
384
- const effect = {
385
- type: effectTypes.contractInvoked,
386
- contract: xdrParseScVal(value.contractAddress()),
387
- function: value.functionName().toString(),
388
- args: rawArgs.map(xdrParseScVal),
389
- rawArgs: nativeToScVal(rawArgs).toXDR('base64')
390
- }
391
- this.addEffect(effect)
392
- }
393
- break
394
- case 'wasm': {
395
- const codeHash = hash(value)
396
- this.addEffect({
397
- type: effectTypes.contractCodeUploaded,
398
- wasm: value.toString('base64'),
399
- wasmHash: codeHash.toString('hex'),
400
- keyHash: generateContractCodeEntryHash(codeHash)
401
- })
402
- break
403
- }
404
- case 'createContract':
405
- case 'createContractV2':
406
- const preimage = value.contractIdPreimage()
407
- const executable = value.executable()
408
- const executableType = executable.switch().name
409
-
410
- const effect = {
411
- type: effectTypes.contractCreated,
412
- contract: contractIdFromPreimage(preimage, this.network)
413
- }
414
- switch (executableType) {
415
- case 'contractExecutableWasm':
416
- effect.kind = 'wasm'
417
- effect.wasmHash = executable.wasmHash().toString('hex')
418
- break
419
- case 'contractExecutableStellarAsset':
420
- const preimageParams = preimage.value()
421
- switch (preimage.switch().name) {
422
- case 'contractIdPreimageFromAddress':
423
- effect.kind = 'fromAddress'
424
- effect.issuer = xdrParseAccountAddress(preimageParams.address().value())
425
- effect.salt = preimageParams.salt().toString('base64')
426
- break
427
- case 'contractIdPreimageFromAsset':
428
- effect.kind = 'fromAsset'
429
- effect.asset = xdrParseAsset(preimageParams)
430
- break
431
- default:
432
- throw new TxMetaEffectParserError('Unknown preimage type: ' + preimage.switch().name)
433
- }
434
- break
435
- default:
436
- throw new TxMetaEffectParserError('Unknown contract type: ' + executableType)
437
- }
438
- if (func.arm() === 'createContractV2') {
439
- const args = value.constructorArgs() //array
440
- if (args.length > 0) {
441
- effect.constructorArgs = args.map(arg => arg.toXDR('base64'))
442
- }
443
- }
444
- this.addEffect(effect, 0)
445
- break
446
- default:
447
- throw new TxMetaEffectParserError('Unknown host function call type: ' + func.arm())
448
- }
449
- }
450
-
451
- bumpFootprintExpiration() {
452
- //const {ledgersToExpire} = this.operation
453
- }
454
-
455
- restoreFootprint() {
456
- }
457
-
458
- setAdmin(contractId, newAdmin) {
459
- const effect = {
460
- type: effectTypes.contractUpdated,
461
- contract: contractId,
462
- admin: newAdmin
463
- }
464
- this.addEffect(effect)
465
- }
466
-
467
- processDexOperationEffects() {
468
- if (!this.result)
469
- return
470
- //process trades first
471
- for (const claimedOffer of this.result.claimedOffers) {
472
- const trade = {
473
- type: effectTypes.trade,
474
- amount: claimedOffer.amount,
475
- asset: claimedOffer.asset
476
- }
477
- if (claimedOffer.poolId) {
478
- trade.pool = claimedOffer.poolId.toString('hex')
479
- } else {
480
- trade.offer = claimedOffer.offerId
481
- trade.seller = claimedOffer.account
482
-
483
- }
484
- this.addEffect(trade)
485
- }
486
- }
487
-
488
- processSponsorshipEffects() {
489
- for (const change of this.changes) {
490
- const {type, action, before, after} = change
491
- const effect = {}
492
- switch (action) {
493
- case 'created':
494
- if (!after.sponsor)
495
- continue
496
- effect.sponsor = after.sponsor
497
- break
498
- case 'updated':
499
- if (before.sponsor === after.sponsor)
500
- continue
501
- effect.sponsor = after.sponsor
502
- effect.prevSponsor = before.sponsor
503
- break
504
- case 'removed':
505
- if (!before.sponsor)
506
- continue
507
- effect.prevSponsor = before.sponsor
508
- break
509
- }
510
- switch (type) {
511
- case 'account':
512
- effect.account = before?.address || after?.address
513
- break
514
- case 'trustline':
515
- effect.account = before?.account || after?.account
516
- effect.asset = before?.asset || after?.asset
517
- break
518
- case 'offer':
519
- effect.account = before?.account || after?.account
520
- effect.offer = before?.id || after?.id
521
- break
522
- case 'data':
523
- effect.account = before?.account || after?.account
524
- effect.name = before?.name || after?.name
525
- break
526
- case 'claimableBalance':
527
- effect.balance = before?.balanceId || after?.balanceId
528
- //TODO: add claimable balance asset to the effect
529
- break
530
- case 'liquidityPool': //ignore??
531
- continue
532
- }
533
- effect.type = encodeSponsorshipEffectName(action, type)
534
- this.addEffect(effect)
535
- }
536
- }
537
-
538
- processAccountChanges({action, before, after}) {
539
- switch (action) {
540
- case 'created':
541
- const accountCreated = {
542
- type: effectTypes.accountCreated,
543
- account: after.address
544
- }
545
- if (after.sponsor) {
546
- accountCreated.sponsor = after.sponsor
547
- }
548
- this.addEffect(accountCreated)
549
- if (after.balance > 0) {
550
- this.credit(after.balance, 'XLM', after.address, after.balance)
551
- }
552
- break
553
- case 'updated':
554
- if (before.balance !== after.balance) {
555
- this.processBalanceChange(after.address, 'XLM', before.balance, after.balance)
556
- }
557
- //other operations do not yield signer sponsorship effects
558
- if (this.operation.type === 'setOptions' || this.operation.type === 'revokeSignerSponsorship') {
559
- this.processSignerSponsorshipEffects({before, after})
560
- }
561
- break
562
- case 'removed':
563
- if (before.balance > 0) {
564
- this.debit(before.balance, 'XLM', before.address, '0')
565
- }
566
- const accountRemoved = {
567
- type: effectTypes.accountRemoved
568
- }
569
- if (before.sponsor) {
570
- accountRemoved.sponsor = before.sponsor
571
- }
572
- this.addEffect(accountRemoved)
573
- break
574
- }
575
-
576
- for (const effect of analyzeSignerChanges(before, after)) {
577
- this.addEffect(effect)
578
- }
579
- }
580
-
581
- processTrustlineEffectsChanges({action, before, after}) {
582
- const snapshot = (after || before)
583
- const trustEffect = {
584
- type: '',
585
- source: snapshot.account,
586
- asset: snapshot.asset,
587
- kind: snapshot.asset.includes('-') ? 'asset' : 'poolShares',
588
- flags: snapshot.flags
589
- }
590
- if (snapshot.sponsor) {
591
- trustEffect.sponsor = snapshot.sponsor
592
- }
593
- switch (action) {
594
- case 'created':
595
- trustEffect.type = effectTypes.trustlineCreated
596
- trustEffect.limit = snapshot.limit
597
- break
598
- case 'updated':
599
- if (before.balance !== after.balance) {
600
- this.processBalanceChange(after.account, after.asset, before.balance, after.balance)
601
- }
602
- if (before.limit === after.limit && before.flags === after.flags)
603
- return
604
- trustEffect.type = effectTypes.trustlineUpdated
605
- trustEffect.limit = snapshot.limit
606
- break
607
- case 'removed':
608
- trustEffect.type = effectTypes.trustlineRemoved
609
- if (before.balance > 0) {
610
- this.processBalanceChange(before.account, before.asset, before.balance, '0')
611
- }
612
- break
613
- }
614
- this.addEffect(trustEffect)
615
- }
616
-
617
- processBalanceChange(account, asset, beforeBalance, afterBalance) {
618
- if (this.isContractCall) { //map contract=>asset proactively
619
- mapSacContract(this, undefined, asset)
620
- }
621
- const balanceChange = BigInt(afterBalance) - BigInt(beforeBalance)
622
- if (balanceChange < 0n) {
623
- this.debit((-balanceChange).toString(), asset, account, afterBalance)
624
- } else {
625
- this.credit(balanceChange.toString(), asset, account, afterBalance)
626
- }
627
- }
628
-
629
- processSignerSponsorshipEffects({before, after}) {
630
- if (!before.signerSponsoringIDs?.length && !after.signerSponsoringIDs?.length)
631
- return
632
- const [beforeMap, afterMap] = [before, after].map(state => {
633
- const signersMap = {}
634
- if (state.signerSponsoringIDs?.length) {
635
- for (let i = 0; i < state.signers.length; i++) {
636
- const sponsor = state.signerSponsoringIDs[i]
637
- if (sponsor) { //add only sponsored signers to the map
638
- signersMap[state.signers[i].key] = sponsor
639
- }
640
- }
641
- }
642
- return signersMap
643
- })
644
-
645
- for (const signerKey of Object.keys(beforeMap)) {
646
- const newSponsor = afterMap[signerKey]
647
- if (!newSponsor) {
648
- this.addEffect({
649
- type: effectTypes.signerSponsorshipRemoved,
650
- account: before.address,
651
- signer: signerKey,
652
- prevSponsor: beforeMap[signerKey]
653
- })
654
- break
655
- }
656
- if (newSponsor !== beforeMap[signerKey]) {
657
- this.addEffect({
658
- type: effectTypes.signerSponsorshipUpdated,
659
- account: before.address,
660
- signer: signerKey,
661
- sponsor: newSponsor,
662
- prevSponsor: beforeMap[signerKey]
663
- })
664
- break
665
- }
666
- }
667
-
668
- for (const signerKey of Object.keys(afterMap)) {
669
- const prevSponsor = beforeMap[signerKey]
670
- if (!prevSponsor) {
671
- this.addEffect({
672
- type: effectTypes.signerSponsorshipCreated,
673
- account: after.address,
674
- signer: signerKey,
675
- sponsor: afterMap[signerKey]
676
- })
677
- break
678
- }
679
- }
680
- }
681
-
682
- processOfferChanges({action, before, after}) {
683
- const snapshot = after || before
684
- const effect = {
685
- type: effectTypes.offerRemoved,
686
- owner: snapshot.account,
687
- offer: snapshot.id,
688
- asset: snapshot.asset,
689
- flags: snapshot.flags
690
- }
691
- if (snapshot.sponsor) {
692
- effect.sponsor = snapshot.sponsor
693
- }
694
- switch (action) {
695
- case 'created':
696
- effect.type = effectTypes.offerCreated
697
- effect.amount = after.amount
698
- effect.price = after.price
699
- break
700
- case 'updated':
701
- if (before.price === after.price && before.asset.join() === after.asset.join() && before.amount === after.amount)
702
- return //no changes - skip
703
- effect.type = effectTypes.offerUpdated
704
- effect.amount = after.amount
705
- effect.price = after.price
706
- break
707
- }
708
- this.addEffect(effect)
709
- }
710
-
711
- processLiquidityPoolChanges({action, before, after}) {
712
- const snapshot = after || before
713
- const effect = {
714
- type: effectTypes.liquidityPoolRemoved,
715
- pool: snapshot.pool
716
- }
717
- if (snapshot.sponsor) {
718
- effect.sponsor = snapshot.sponsor
719
- }
720
- switch (action) {
721
- case 'created':
722
- Object.assign(effect, {
723
- type: effectTypes.liquidityPoolCreated,
724
- reserves: after.asset.map(asset => ({asset, amount: '0'})),
725
- shares: '0',
726
- accounts: 1
727
- })
728
- this.addEffect(effect, this.effects.findIndex(e => e.pool === effect.pool || e.asset === effect.pool))
729
- return
730
- case 'updated':
731
- Object.assign(effect, {
732
- type: effectTypes.liquidityPoolUpdated,
733
- reserves: after.asset.map((asset, i) => ({
734
- asset,
735
- amount: after.amount[i]
736
- })),
737
- shares: after.shares,
738
- accounts: after.accounts
739
- })
740
- break
741
- }
742
- this.addEffect(effect)
743
- }
744
-
745
- processClaimableBalanceChanges({action, before, after}) {
746
- switch (action) {
747
- case 'created':
748
- this.addEffect({
749
- type: effectTypes.claimableBalanceCreated,
750
- sponsor: after.sponsor,
751
- balance: after.balanceId,
752
- asset: after.asset,
753
- amount: after.amount,
754
- claimants: after.claimants
755
- })
756
- break
757
- case 'removed':
758
- this.addEffect({
759
- type: effectTypes.claimableBalanceRemoved,
760
- sponsor: before.sponsor,
761
- balance: before.balanceId,
762
- asset: before.asset,
763
- amount: before.amount,
764
- claimants: before.claimants
765
- })
766
- break
767
- case 'updated':
768
- //nothing to process here
769
- break
770
- }
771
- }
772
-
773
- processDataEntryChanges({action, before, after}) {
774
- const effect = {type: ''}
775
- const {sponsor, name, value} = after || before
776
- effect.name = name
777
- effect.value = value && value.toString('base64')
778
- switch (action) {
779
- case 'created':
780
- effect.type = effectTypes.dataEntryCreated
781
- break
782
- case 'updated':
783
- if (before.value === after.value)
784
- return //value has not changed
785
- effect.type = effectTypes.dataEntryUpdated
786
- break
787
- case 'removed':
788
- effect.type = effectTypes.dataEntryRemoved
789
- delete effect.value
790
- break
791
- }
792
- if (sponsor) {
793
- effect.sponsor = sponsor
794
- }
795
- this.addEffect(effect)
796
- }
797
-
798
- processContractBalance(effect) {
799
- const parsedKey = xdr.ScVal.fromXDR(effect.key, 'base64')
800
- if (parsedKey._arm !== 'vec')
801
- return
802
- const keyParts = parsedKey._value
803
- if (!(keyParts instanceof Array) || keyParts.length !== 2)
804
- return
805
- if (keyParts[0]._arm !== 'sym' || keyParts[1]._arm !== 'address' || keyParts[0]._value.toString() !== 'Balance')
806
- return
807
- const account = xdrParseScVal(keyParts[1])
808
- const balanceEffects = this.effects.filter(e => (e.type === effectTypes.accountCredited || e.type === effectTypes.accountDebited) && e.source === account && e.asset === effect.owner)
809
- if (balanceEffects.length !== 1) //we can set balance only when we found 1-1 mapping, if there are several balance changes, we can't establish balance relation
810
- return
811
- if (effect.type === effectTypes.contractDataRemoved) { //balance completely removed - this may be a reversible operation if the balance simply expired
812
- balanceEffects[0].balance = '0'
813
- return
814
- }
815
- const value = xdr.ScVal.fromXDR(effect.value, 'base64')
816
- if (value._arm !== 'map')
817
- return
818
- const parsedValue = xdrParseScVal(value)
819
- if (typeof parsedValue.clawback !== 'boolean' || typeof parsedValue.authorized !== 'boolean' || typeof parsedValue.amount !== 'string')
820
- return
821
- //set transfer effect balance
822
- balanceEffects[0].balance = parsedValue.amount
823
- }
824
-
825
- processContractChanges({action, before, after}) {
826
- if (action !== 'created' && action !== 'updated')
827
- throw new UnexpectedTxMetaChangeError({type: 'contract', action})
828
- const {kind, owner: contract, keyHash} = after
829
- let effect = {
830
- type: effectTypes.contractCreated,
831
- contract,
832
- kind
833
- }
834
- switch (kind) {
835
- case 'fromAsset':
836
- effect.asset = after.asset
837
- break
838
- case 'wasm':
839
- effect.wasmHash = after.wasmHash
840
- break
841
- default:
842
- throw new TxMetaEffectParserError('Unexpected contract type: ' + kind)
843
- }
844
- if (action === 'created') {
845
- if (this.effects.some(e => e.type === effectTypes.contractCreated && e.contract === contract)) {
846
- effect = undefined //skip contract creation effects processed by top-level createContract operation call
847
- }
848
- } else if (action === 'updated') {
849
- effect.type = effectTypes.contractUpdated
850
- effect.prevWasmHash = before.wasmHash
851
- if (before.wasmHash === after.wasmHash) {//skip if hash unchanged
852
- effect = undefined
853
- }
854
- }
855
- if (effect) {
856
- this.addEffect(effect, effect.type === effectTypes.contractCreated ? 0 : undefined)
857
- }
858
- if (before?.storage?.length || after?.storage?.length) {
859
- this.processInstanceDataChanges(before, after)
860
- }
861
- }
862
-
863
- processContractStateEntryChanges({action, before, after}) {
864
- const {owner, key, durability, keyHash} = after || before
865
- const effect = {
866
- type: '',
867
- owner,
868
- key,
869
- durability,
870
- keyHash
871
- }
872
- switch (action) {
873
- case 'created':
874
- effect.type = effectTypes.contractDataCreated
875
- effect.value = after.value
876
- break
877
- case 'updated':
878
- if (before.value === after.value)
879
- return //value has not changed
880
- effect.type = effectTypes.contractDataUpdated
881
- effect.value = after.value
882
- effect.prevValue = before.value
883
- break
884
- case 'removed':
885
- effect.type = effectTypes.contractDataRemoved
886
- effect.prevValue = before.value
887
- break
888
- }
889
- this.addEffect(effect)
890
- this.processContractBalance(effect)
891
- }
892
-
893
- processContractCodeChanges({type, action, before, after}) {
894
- const {hash, keyHash} = after || before
895
- switch (action) {
896
- case 'created':
897
- break //processed separately
898
- case 'updated':
899
- break //it doesn't change the state
900
- case 'removed':
901
- const effect = {
902
- type: effectTypes.contractCodeRemoved,
903
- wasmHash: hash,
904
- keyHash
905
- }
906
- this.addEffect(effect)
907
- break
908
- }
909
- }
910
-
911
- processInstanceDataChanges(before, after) {
912
- const storageBefore = before?.storage || []
913
- const storageAfter = [...(after?.storage || [])]
914
- for (const {key, val} of storageBefore) {
915
- let newVal
916
- for (let i = 0; i < storageAfter.length; i++) {
917
- const afterValue = storageAfter[i]
918
- if (afterValue.key === key) {
919
- newVal = afterValue.val //update new value
920
- storageAfter.splice(i, 1) //remove from array to simplify iteration
921
- break
922
- }
923
- }
924
- if (newVal === undefined) { //removed
925
- const effect = {
926
- type: effectTypes.contractDataRemoved,
927
- owner: after?.owner || before.owner,
928
- key,
929
- prevValue: val,
930
- durability: 'instance'
931
- }
932
- this.addEffect(effect)
933
- continue
934
- }
935
- if (val === newVal) //value has not changed
936
- continue
937
-
938
- const effect = {
939
- type: effectTypes.contractDataUpdated,
940
- owner: after?.owner || before.owner,
941
- key,
942
- value: newVal,
943
- prevValue: val,
944
- durability: 'instance'
945
- }
946
- this.addEffect(effect)
947
- }
948
- //iterate all storage items left
949
- for (const {key, val} of storageAfter) {
950
- const effect = {
951
- type: effectTypes.contractDataCreated,
952
- owner: after?.owner || before.owner,
953
- key,
954
- value: val,
955
- durability: 'instance'
956
- }
957
- this.addEffect(effect)
958
- }
959
- }
960
-
961
- processTtlChanges({action, before, after}) {
962
- /*if (action === 'removed')
963
- throw new UnexpectedTxMetaChangeError({type: 'ttl', action})*/
964
- const {keyHash, ttl} = after || before
965
- const stateEffect = this.effects.find(e => e.keyHash === keyHash && e.type !== effectTypes.setTtl)
966
- const effect = {
967
- type: effectTypes.setTtl,
968
- keyHash,
969
- ttl
970
- }
971
- if (stateEffect) {
972
- if (stateEffect.type.includes('contractCode')) {
973
- effect.kind = 'contractCode'
974
- } else if (stateEffect.type.includes('contractData')) {
975
- effect.kind = 'contractData'
976
- effect.owner = stateEffect.owner
977
- }
978
- }
979
- this.addEffect(effect)
980
- }
981
-
982
- processChanges() {
983
- for (const change of this.changes)
984
- switch (change.type) {
985
- case 'account':
986
- this.processAccountChanges(change)
987
- break
988
- case 'trustline':
989
- this.processTrustlineEffectsChanges(change)
990
- break
991
- case 'claimableBalance':
992
- this.processClaimableBalanceChanges(change)
993
- break
994
- case 'offer':
995
- this.processOfferChanges(change)
996
- break
997
- case 'liquidityPool':
998
- this.processLiquidityPoolChanges(change)
999
- break
1000
- case 'data':
1001
- this.processDataEntryChanges(change)
1002
- break
1003
- case 'contractData':
1004
- if (change.before?.kind || change.after?.kind) {
1005
- this.processContractChanges(change)
1006
- }
1007
- break
1008
- case 'contractCode':
1009
- this.processContractCodeChanges(change)
1010
- break
1011
- case 'ttl':
1012
- this.processTtlChanges(change)
1013
- break
1014
- default:
1015
- throw new UnexpectedTxMetaChangeError(change)
1016
- }
1017
- }
1018
-
1019
- processStateChanges() {
1020
- for (const change of this.changes)
1021
- if (change.type === 'contractData') {
1022
- this.processContractStateEntryChanges(change)
1023
- }
1024
- }
1025
-
1026
- retrieveOpContractId() {
1027
- const funcValue = this.operation.func._value._attributes
1028
- if (funcValue) {
1029
- if (funcValue.contractAddress)
1030
- return StrKey.encodeContract(funcValue.contractAddress._value)
1031
- const preimage = funcValue.contractIdPreimage
1032
- if (preimage)
1033
- return contractIdFromPreimage(preimage, this.network)
1034
- }
1035
- return null
1036
- }
1037
-
1038
- resolveAsset(assetOrContract) {
1039
- if (!assetOrContract.startsWith('C') || !this.sacMap)
1040
- return assetOrContract
1041
- //try to resolve using SAC map
1042
- return this.sacMap[assetOrContract] || assetOrContract
1043
- }
1044
- }
1045
-
1046
- /**
1047
- * Generates fee charged effect
1048
- * @param {{}} tx - Transaction
1049
- * @param {String} source - Source account
1050
- * @param {String} chargedAmount - Charged amount
1051
- * @param {Boolean} [feeBump] - Is fee bump transaction
1052
- * @returns {{}} - Fee charged effect
1053
- */
1054
- function processFeeChargedEffect(tx, source, chargedAmount, feeBump = false) {
1055
- if (tx._switch) { //raw XDR
1056
- const txXdr = tx.value().tx()
1057
- tx = {
1058
- source: xdrParseAccountAddress((txXdr.feeSource ? txXdr.feeSource : txXdr.sourceAccount).call(txXdr)),
1059
- fee: txXdr.fee().toString()
1060
- }
1061
- }
1062
- const res = {
1063
- type: effectTypes.feeCharged,
1064
- source,
1065
- asset: 'XLM',
1066
- bid: tx.fee,
1067
- charged: chargedAmount
1068
- }
1069
- if (feeBump) {
1070
- res.bump = true
1071
- }
1072
- return res
1073
- }
1074
-
1075
- function normalizeAddress(address) {
1076
- const prefix = address[0]
1077
- if (prefix === 'G')
1078
- return address
1079
- if (prefix !== 'M')
1080
- throw new TypeError('Expected ED25519 or Muxed address')
1081
- const rawBytes = StrKey.decodeMed25519PublicKey(address)
1082
- return StrKey.encodeEd25519PublicKey(rawBytes.subarray(0, 32))
1083
- }
1084
-
1085
-
1086
- /**
1087
- * @param {String} action
1088
- * @param {String} type
1089
- * @return {String}
1090
- */
1091
- function encodeSponsorshipEffectName(action, type) {
1092
- let actionKey
1093
- switch (action) {
1094
- case 'created':
1095
- actionKey = 'Created'
1096
- break
1097
- case 'updated':
1098
- actionKey = 'Updated'
1099
- break
1100
- case 'removed':
1101
- actionKey = 'Removed'
1102
- break
1103
- default:
1104
- throw new UnexpectedTxMetaChangeError({action, type})
1105
- }
1106
- return effectTypes[`${type}Sponsorship${actionKey}`]
1107
- }
1108
-
1109
- function validateAmount(amount) {
1110
- if (amount < 0)
1111
- throw new TxMetaEffectParserError('Negative balance change amount: ' + amount.toString())
1112
- return amount
1113
- }
1114
-
1115
- /**
1116
- * @param largeInt
1117
- * @return {String}
1118
- */
1119
- function parseLargeInt(largeInt) {
1120
- return largeInt._value.toString()
1121
- }
1122
-
1123
- module.exports = {EffectsAnalyzer, processFeeChargedEffect}
1
+ const {StrKey, hash, xdr, nativeToScVal} = require('@stellar/stellar-base')
2
+ const effectTypes = require('./effect-types')
3
+ const {validateAmount, normalizeAddress, parseLargeInt} = require('./parser/normalization')
4
+ const {parseLedgerEntryChanges} = require('./parser/ledger-entry-changes-parser')
5
+ const {xdrParseAsset, xdrParseAccountAddress, xdrParseScVal} = require('./parser/tx-xdr-parser-utils')
6
+ const {contractIdFromPreimage} = require('./parser/contract-preimage-encoder')
7
+ const {generateContractCodeEntryHash} = require('./parser/ledger-key')
8
+ const {analyzeSignerChanges} = require('./aggregation/signer-changes-analyzer')
9
+ const EventsAnalyzer = require('./aggregation/events-analyzer')
10
+ const AssetSupplyAnalyzer = require('./aggregation/asset-supply-analyzer')
11
+ const {mapSacContract} = require('./aggregation/sac-contract-mapper')
12
+ const {UnexpectedTxMetaChangeError, TxMetaEffectParserError} = require('./errors')
13
+
14
+ class EffectsAnalyzer {
15
+ constructor({
16
+ operation,
17
+ meta,
18
+ result,
19
+ network,
20
+ events,
21
+ diagnosticEvents,
22
+ mapSac,
23
+ processSystemEvents,
24
+ processFailedOpEffects,
25
+ processMetrics
26
+ }) {
27
+ //set execution context
28
+ if (!operation.source)
29
+ throw new TxMetaEffectParserError('Operation source is not explicitly defined')
30
+ this.operation = operation
31
+ this.isContractCall = this.operation.type === 'invokeHostFunction'
32
+ this.result = result
33
+ this.changes = parseLedgerEntryChanges(meta)
34
+ this.source = this.operation.source
35
+ this.events = events
36
+ this.processFailedOpEffects = processFailedOpEffects
37
+ this.processMetrics = processMetrics
38
+ if (diagnosticEvents?.length) {
39
+ this.diagnosticEvents = diagnosticEvents
40
+ if (processSystemEvents) {
41
+ this.processSystemEvents = true
42
+ }
43
+ }
44
+ this.network = network
45
+ if (mapSac) {
46
+ this.sacMap = new Map()
47
+ }
48
+ }
49
+
50
+ /**
51
+ * @type {{}[]}
52
+ * @internal
53
+ * @readonly
54
+ */
55
+ effects = []
56
+ /**
57
+ * @type {Object}
58
+ * @private
59
+ * @readonly
60
+ */
61
+ operation = null
62
+ /**
63
+ * @type {String}
64
+ * @readonly
65
+ */
66
+ network
67
+ /**
68
+ * @type {Map<string,string>}
69
+ * @readonly
70
+ */
71
+ sacMap
72
+ /**
73
+ * @type {ParsedLedgerEntryMeta[]}
74
+ * @private
75
+ * @readonly
76
+ */
77
+ changes = null
78
+ /**
79
+ * @type {Object}
80
+ * @private
81
+ * @readonly
82
+ */
83
+ result = null
84
+ /**
85
+ * @type {String}
86
+ * @private
87
+ * @readonly
88
+ */
89
+ source = ''
90
+ /**
91
+ * @type {Boolean}
92
+ * @private
93
+ */
94
+ isContractCall = false
95
+ /**
96
+ * @type {Boolean}
97
+ * @readonly
98
+ */
99
+ processSystemEvents = false
100
+ /**
101
+ * @type {Boolean}
102
+ * @readonly
103
+ */
104
+ processMetrics = true
105
+ /**
106
+ * @type {{}}
107
+ * @private
108
+ */
109
+ metrics
110
+
111
+ analyze() {
112
+ //find appropriate parser method
113
+ const parse = this[this.operation.type]
114
+ if (parse) {
115
+ parse.call(this)
116
+ }
117
+ //process Soroban events
118
+ new EventsAnalyzer(this).analyze()
119
+ //process state data changes in the end
120
+ this.processStateChanges()
121
+ //process ledger entry changes
122
+ this.processChanges()
123
+ //handle effects that are processed indirectly
124
+ this.processSponsorshipEffects()
125
+ //calculate minted/burned assets
126
+ new AssetSupplyAnalyzer(this).analyze()
127
+ //add Soroban op metrics if available
128
+ if (this.metrics) {
129
+ this.addEffect(this.metrics)
130
+ }
131
+ return this.effects
132
+ }
133
+
134
+ /**
135
+ * @param {{}} effect
136
+ * @param {Number} [atPosition]
137
+ */
138
+ addEffect(effect, atPosition) {
139
+ if (!effect.source) {
140
+ effect.source = this.source
141
+ }
142
+ if (atPosition !== undefined) {
143
+ this.effects.splice(atPosition < 0 ? 0 : atPosition, 0, effect)
144
+ } else {
145
+ this.effects.push(effect)
146
+ }
147
+ }
148
+
149
+ debit(amount, asset, source, balance) {
150
+ if (amount === '0')
151
+ return
152
+ const effect = {
153
+ type: effectTypes.accountDebited,
154
+ source,
155
+ asset,
156
+ amount: validateAmount(amount)
157
+ }
158
+ if (balance !== undefined) {
159
+ effect.balance = balance
160
+ }
161
+ this.addEffect(effect)
162
+ }
163
+
164
+ credit(amount, asset, source, balance) {
165
+ if (amount === '0')
166
+ return
167
+ const effect = {
168
+ type: effectTypes.accountCredited,
169
+ source,
170
+ asset,
171
+ amount: validateAmount(amount)
172
+ }
173
+ if (balance !== undefined) {
174
+ effect.balance = balance
175
+ }
176
+ this.addEffect(effect)
177
+ }
178
+
179
+ mint(asset, amount, autoLookupPosition = false) {
180
+ const position = autoLookupPosition ?
181
+ this.effects.findIndex(e => e.asset === asset || e.assets?.find(a => a.asset === asset)) :
182
+ undefined
183
+ this.addEffect({
184
+ type: effectTypes.assetMinted,
185
+ asset,
186
+ amount: validateAmount(amount)
187
+ }, position)
188
+ }
189
+
190
+ burn(asset, amount, position = undefined) {
191
+ this.addEffect({
192
+ type: effectTypes.assetBurned,
193
+ asset,
194
+ amount: validateAmount(amount)
195
+ }, position)
196
+ }
197
+
198
+ addMetric(contract, metric, value) {
199
+ let {metrics} = this
200
+ if (!metrics) {
201
+ metrics = this.metrics = {
202
+ type: effectTypes.contractMetrics,
203
+ contract
204
+ }
205
+ }
206
+ metrics[metric] = value
207
+ }
208
+
209
+ addFeeMetric(metaValue) {
210
+ const {sorobanMeta} = metaValue._attributes
211
+ if (!sorobanMeta)
212
+ return
213
+ const sorobanExt = sorobanMeta._attributes.ext._value
214
+ if (sorobanExt) {
215
+ const attrs = sorobanExt._attributes
216
+ const fee = {
217
+ nonrefundable: parseInt(parseLargeInt(attrs.totalNonRefundableResourceFeeCharged)),
218
+ refundable: parseInt(parseLargeInt(attrs.totalRefundableResourceFeeCharged)),
219
+ rent: parseInt(parseLargeInt(attrs.rentFeeCharged))
220
+ }
221
+ this.addMetric(this.retrieveOpContractId(), 'fee', fee)
222
+ }
223
+ }
224
+
225
+ setOptions() {
226
+ const sourceAccount = normalizeAddress(this.source)
227
+ const change = this.changes.find(ch => ch.type === 'account' && ch.before.address === sourceAccount)
228
+ if (!change)
229
+ return // failed tx or no changes
230
+ const {before, after} = change
231
+ if (before.homeDomain !== after.homeDomain) {
232
+ this.addEffect({
233
+ type: effectTypes.accountHomeDomainUpdated,
234
+ domain: after.homeDomain
235
+ })
236
+ }
237
+ if (before.thresholds !== after.thresholds) {
238
+ this.addEffect({
239
+ type: effectTypes.accountThresholdsUpdated,
240
+ thresholds: after.thresholds.split(',').map(v => parseInt(v, 10))
241
+ })
242
+ }
243
+ if (before.flags !== after.flags) {
244
+ this.addEffect({
245
+ type: effectTypes.accountFlagsUpdated,
246
+ flags: after.flags,
247
+ prevFlags: before.flags
248
+ })
249
+ }
250
+ if (before.inflationDest !== after.inflationDest) {
251
+ this.addEffect({
252
+ type: effectTypes.accountInflationDestinationUpdated,
253
+ inflationDestination: after.inflationDest
254
+ })
255
+ }
256
+ }
257
+
258
+ allowTrust() {
259
+ this.setTrustLineFlags()
260
+ }
261
+
262
+ setTrustLineFlags() {
263
+ if (!this.changes.length)
264
+ return
265
+ const trustAsset = xdrParseAsset(this.operation.asset || {
266
+ code: this.operation.assetCode,
267
+ issuer: normalizeAddress(this.source)
268
+ })
269
+ const change = this.changes.find(ch => ch.type === 'trustline' && ch.before.asset === trustAsset)
270
+ if (!change)
271
+ return
272
+ if (change.action !== 'updated')
273
+ throw new UnexpectedTxMetaChangeError(change)
274
+ const {before, after} = change
275
+ if (before.flags !== after.flags) {
276
+ this.addEffect({
277
+ type: effectTypes.trustlineAuthorizationUpdated,
278
+ trustor: this.operation.trustor,
279
+ asset: after.asset,
280
+ flags: after.flags,
281
+ prevFlags: before.flags
282
+ })
283
+ for (const change of this.changes) {
284
+ if (change.type !== 'liquidityPool')
285
+ continue
286
+ const {before, after} = change
287
+ this.addEffect({
288
+ type: effectTypes.liquidityPoolWithdrew,
289
+ source: this.operation.trustor,
290
+ pool: before.pool,
291
+ assets: before.asset.map((asset, i) => ({
292
+ asset,
293
+ amount: (BigInt(before.amount[i]) - (after ? BigInt(after.amount[i]) : 0n)).toString()
294
+ })),
295
+ shares: (BigInt(before.shares) - (after ? BigInt(after.shares) : 0n)).toString()
296
+ })
297
+ }
298
+ }
299
+ }
300
+
301
+ inflation() {
302
+ /*const paymentEffects = (result.inflationPayouts || []).map(ip => ({
303
+ type: effectTypes.accountCredited,
304
+ source: ip.account,
305
+ asset: 'XLM',
306
+ amount: ip.amount
307
+ }))*/
308
+ this.addEffect({type: effectTypes.inflation})
309
+ }
310
+
311
+ bumpSequence() {
312
+ if (!this.changes.length)
313
+ return
314
+ const change = this.changes.find(ch => ch.type === 'account')
315
+ if (!change)
316
+ return //failed tx or no changes
317
+ const {before, after} = change
318
+ if (before.sequence !== after.sequence) {
319
+ this.addEffect({
320
+ type: effectTypes.sequenceBumped,
321
+ sequence: after.sequence
322
+ })
323
+ }
324
+ }
325
+
326
+ pathPaymentStrictReceive() {
327
+ this.processDexOperationEffects()
328
+ }
329
+
330
+ pathPaymentStrictSend() {
331
+ this.processDexOperationEffects()
332
+ }
333
+
334
+ manageSellOffer() {
335
+ this.processDexOperationEffects()
336
+ }
337
+
338
+ manageBuyOffer() {
339
+ this.processDexOperationEffects()
340
+ }
341
+
342
+ createPassiveSellOffer() {
343
+ this.processDexOperationEffects()
344
+ }
345
+
346
+ liquidityPoolDeposit() {
347
+ const {liquidityPoolId} = this.operation
348
+ const change = this.changes.find(ch => ch.type === 'liquidityPool' && ch.action === 'updated' && ch.after.pool === liquidityPoolId)
349
+ if (!change) //tx failed
350
+ return
351
+ const {before, after} = change
352
+ this.addEffect({
353
+ type: effectTypes.liquidityPoolDeposited,
354
+ pool: this.operation.liquidityPoolId,
355
+ assets: after.asset.map((asset, i) => ({
356
+ asset,
357
+ amount: (after.amount[i] - before.amount[i]).toString()
358
+ })),
359
+ shares: (after.shares - before.shares).toString(),
360
+ accounts: after.accounts
361
+ })
362
+ }
363
+
364
+ liquidityPoolWithdraw() {
365
+ const pool = this.operation.liquidityPoolId
366
+ const change = this.changes.find(ch => ch.type === 'liquidityPool' && ch.action === 'updated' && ch.before.pool === pool)
367
+ if (!change) //tx failed
368
+ return
369
+ const {before, after} = change
370
+ this.addEffect({
371
+ type: effectTypes.liquidityPoolWithdrew,
372
+ pool,
373
+ assets: before.asset.map((asset, i) => ({
374
+ asset,
375
+ amount: (before.amount[i] - after.amount[i]).toString()
376
+ })),
377
+ shares: (before.shares - after.shares).toString(),
378
+ accounts: after.accounts
379
+ })
380
+ }
381
+
382
+ invokeHostFunction() {
383
+ const {func} = this.operation
384
+ const value = func.value()
385
+ switch (func.arm()) {
386
+ case 'invokeContract':
387
+ if (!this.diagnosticEvents) {
388
+ //add top-level contract invocation effect only if diagnostic events are unavailable
389
+ const rawArgs = value.args()
390
+ const effect = {
391
+ type: effectTypes.contractInvoked,
392
+ contract: xdrParseScVal(value.contractAddress()),
393
+ function: value.functionName().toString(),
394
+ args: rawArgs.map(xdrParseScVal),
395
+ rawArgs: nativeToScVal(rawArgs).toXDR('base64')
396
+ }
397
+ this.addEffect(effect)
398
+ }
399
+ break
400
+ case 'wasm': {
401
+ const codeHash = hash(value)
402
+ this.addEffect({
403
+ type: effectTypes.contractCodeUploaded,
404
+ wasm: value.toString('base64'),
405
+ wasmHash: codeHash.toString('hex'),
406
+ keyHash: generateContractCodeEntryHash(codeHash)
407
+ })
408
+ break
409
+ }
410
+ case 'createContract':
411
+ case 'createContractV2':
412
+ const preimage = value.contractIdPreimage()
413
+ const executable = value.executable()
414
+ const executableType = executable.switch().name
415
+
416
+ const effect = {
417
+ type: effectTypes.contractCreated,
418
+ contract: contractIdFromPreimage(preimage, this.network)
419
+ }
420
+ switch (executableType) {
421
+ case 'contractExecutableWasm':
422
+ effect.kind = 'wasm'
423
+ effect.wasmHash = executable.wasmHash().toString('hex')
424
+ break
425
+ case 'contractExecutableStellarAsset':
426
+ const preimageParams = preimage.value()
427
+ switch (preimage.switch().name) {
428
+ case 'contractIdPreimageFromAddress':
429
+ effect.kind = 'fromAddress'
430
+ effect.issuer = xdrParseAccountAddress(preimageParams.address().value())
431
+ effect.salt = preimageParams.salt().toString('base64')
432
+ break
433
+ case 'contractIdPreimageFromAsset':
434
+ effect.kind = 'fromAsset'
435
+ effect.asset = xdrParseAsset(preimageParams)
436
+ break
437
+ default:
438
+ throw new TxMetaEffectParserError('Unknown preimage type: ' + preimage.switch().name)
439
+ }
440
+ break
441
+ default:
442
+ throw new TxMetaEffectParserError('Unknown contract type: ' + executableType)
443
+ }
444
+ if (func.arm() === 'createContractV2') {
445
+ const args = value.constructorArgs() //array
446
+ if (args.length > 0) {
447
+ effect.constructorArgs = args.map(arg => arg.toXDR('base64'))
448
+ }
449
+ }
450
+ this.addEffect(effect, 0)
451
+ break
452
+ default:
453
+ throw new TxMetaEffectParserError('Unknown host function call type: ' + func.arm())
454
+ }
455
+ }
456
+
457
+ bumpFootprintExpiration() {
458
+ //const {ledgersToExpire} = this.operation
459
+ }
460
+
461
+ restoreFootprint() {
462
+ }
463
+
464
+ setAdmin(contractId, newAdmin) {
465
+ const effect = {
466
+ type: effectTypes.contractUpdated,
467
+ contract: contractId,
468
+ admin: newAdmin
469
+ }
470
+ this.addEffect(effect)
471
+ }
472
+
473
+ processDexOperationEffects() {
474
+ if (!this.result)
475
+ return
476
+ //process trades first
477
+ for (const claimedOffer of this.result.claimedOffers) {
478
+ const trade = {
479
+ type: effectTypes.trade,
480
+ amount: claimedOffer.amount,
481
+ asset: claimedOffer.asset
482
+ }
483
+ if (claimedOffer.poolId) {
484
+ trade.pool = claimedOffer.poolId.toString('hex')
485
+ } else {
486
+ trade.offer = claimedOffer.offerId
487
+ trade.seller = claimedOffer.account
488
+
489
+ }
490
+ this.addEffect(trade)
491
+ }
492
+ }
493
+
494
+ processSponsorshipEffects() {
495
+ for (const change of this.changes) {
496
+ const {type, action, before, after} = change
497
+ const effect = {}
498
+ switch (action) {
499
+ case 'created':
500
+ case 'restored':
501
+ if (!after.sponsor)
502
+ continue
503
+ effect.sponsor = after.sponsor
504
+ break
505
+ case 'updated':
506
+ if (before.sponsor === after.sponsor)
507
+ continue
508
+ effect.sponsor = after.sponsor
509
+ effect.prevSponsor = before.sponsor
510
+ break
511
+ case 'removed':
512
+ if (!before.sponsor)
513
+ continue
514
+ effect.prevSponsor = before.sponsor
515
+ break
516
+ }
517
+ switch (type) {
518
+ case 'account':
519
+ effect.account = before?.address || after?.address
520
+ break
521
+ case 'trustline':
522
+ effect.account = before?.account || after?.account
523
+ effect.asset = before?.asset || after?.asset
524
+ break
525
+ case 'offer':
526
+ effect.account = before?.account || after?.account
527
+ effect.offer = before?.id || after?.id
528
+ break
529
+ case 'data':
530
+ effect.account = before?.account || after?.account
531
+ effect.name = before?.name || after?.name
532
+ break
533
+ case 'claimableBalance':
534
+ effect.balance = before?.balanceId || after?.balanceId
535
+ //TODO: add claimable balance asset to the effect
536
+ break
537
+ case 'liquidityPool': //ignore??
538
+ continue
539
+ }
540
+ effect.type = encodeSponsorshipEffectName(action, type)
541
+ this.addEffect(effect)
542
+ }
543
+ }
544
+
545
+ processAccountChanges({action, before, after}) {
546
+ switch (action) {
547
+ case 'created':
548
+ const accountCreated = {
549
+ type: effectTypes.accountCreated,
550
+ account: after.address
551
+ }
552
+ if (after.sponsor) {
553
+ accountCreated.sponsor = after.sponsor
554
+ }
555
+ this.addEffect(accountCreated)
556
+ if (after.balance > 0) {
557
+ this.credit(after.balance, 'XLM', after.address, after.balance)
558
+ }
559
+ break
560
+ case 'updated':
561
+ if (before.balance !== after.balance) {
562
+ this.processBalanceChange(after.address, 'XLM', before.balance, after.balance)
563
+ }
564
+ //other operations do not yield signer sponsorship effects
565
+ if (this.operation.type === 'setOptions' || this.operation.type === 'revokeSignerSponsorship') {
566
+ this.processSignerSponsorshipEffects({before, after})
567
+ }
568
+ break
569
+ case 'removed':
570
+ if (before.balance > 0) {
571
+ this.debit(before.balance, 'XLM', before.address, '0')
572
+ }
573
+ const accountRemoved = {
574
+ type: effectTypes.accountRemoved
575
+ }
576
+ if (before.sponsor) {
577
+ accountRemoved.sponsor = before.sponsor
578
+ }
579
+ this.addEffect(accountRemoved)
580
+ break
581
+ }
582
+
583
+ for (const effect of analyzeSignerChanges(before, after)) {
584
+ this.addEffect(effect)
585
+ }
586
+ }
587
+
588
+ processTrustlineEffectsChanges({action, before, after}) {
589
+ const snapshot = (after || before)
590
+ const trustEffect = {
591
+ type: '',
592
+ source: snapshot.account,
593
+ asset: snapshot.asset,
594
+ kind: snapshot.asset.includes('-') ? 'asset' : 'poolShares',
595
+ flags: snapshot.flags
596
+ }
597
+ if (snapshot.sponsor) {
598
+ trustEffect.sponsor = snapshot.sponsor
599
+ }
600
+ switch (action) {
601
+ case 'created':
602
+ trustEffect.type = effectTypes.trustlineCreated
603
+ trustEffect.limit = snapshot.limit
604
+ break
605
+ case 'updated':
606
+ if (before.balance !== after.balance) {
607
+ this.processBalanceChange(after.account, after.asset, before.balance, after.balance)
608
+ }
609
+ if (before.limit === after.limit && before.flags === after.flags)
610
+ return
611
+ trustEffect.type = effectTypes.trustlineUpdated
612
+ trustEffect.limit = snapshot.limit
613
+ trustEffect.prevFlags = before.flags
614
+ break
615
+ case 'removed':
616
+ trustEffect.type = effectTypes.trustlineRemoved
617
+ if (before.balance > 0) {
618
+ this.processBalanceChange(before.account, before.asset, before.balance, '0')
619
+ }
620
+ break
621
+ }
622
+ this.addEffect(trustEffect)
623
+ }
624
+
625
+ processBalanceChange(account, asset, beforeBalance, afterBalance) {
626
+ if (this.isContractCall) { //map contract=>asset proactively
627
+ mapSacContract(this, undefined, asset)
628
+ }
629
+ const balanceChange = BigInt(afterBalance) - BigInt(beforeBalance)
630
+ if (balanceChange < 0n) {
631
+ this.debit((-balanceChange).toString(), asset, account, afterBalance)
632
+ } else {
633
+ this.credit(balanceChange.toString(), asset, account, afterBalance)
634
+ }
635
+ }
636
+
637
+ processSignerSponsorshipEffects({before, after}) {
638
+ if (!before.signerSponsoringIDs?.length && !after.signerSponsoringIDs?.length)
639
+ return
640
+ const [beforeMap, afterMap] = [before, after].map(state => {
641
+ const signersMap = {}
642
+ if (state.signerSponsoringIDs?.length) {
643
+ for (let i = 0; i < state.signers.length; i++) {
644
+ const sponsor = state.signerSponsoringIDs[i]
645
+ if (sponsor) { //add only sponsored signers to the map
646
+ signersMap[state.signers[i].key] = sponsor
647
+ }
648
+ }
649
+ }
650
+ return signersMap
651
+ })
652
+
653
+ for (const signerKey of Object.keys(beforeMap)) {
654
+ const newSponsor = afterMap[signerKey]
655
+ if (!newSponsor) {
656
+ this.addEffect({
657
+ type: effectTypes.signerSponsorshipRemoved,
658
+ account: before.address,
659
+ signer: signerKey,
660
+ prevSponsor: beforeMap[signerKey]
661
+ })
662
+ break
663
+ }
664
+ if (newSponsor !== beforeMap[signerKey]) {
665
+ this.addEffect({
666
+ type: effectTypes.signerSponsorshipUpdated,
667
+ account: before.address,
668
+ signer: signerKey,
669
+ sponsor: newSponsor,
670
+ prevSponsor: beforeMap[signerKey]
671
+ })
672
+ break
673
+ }
674
+ }
675
+
676
+ for (const signerKey of Object.keys(afterMap)) {
677
+ const prevSponsor = beforeMap[signerKey]
678
+ if (!prevSponsor) {
679
+ this.addEffect({
680
+ type: effectTypes.signerSponsorshipCreated,
681
+ account: after.address,
682
+ signer: signerKey,
683
+ sponsor: afterMap[signerKey]
684
+ })
685
+ break
686
+ }
687
+ }
688
+ }
689
+
690
+ processOfferChanges({action, before, after}) {
691
+ const snapshot = after || before
692
+ const effect = {
693
+ type: effectTypes.offerRemoved,
694
+ owner: snapshot.account,
695
+ offer: snapshot.id,
696
+ asset: snapshot.asset,
697
+ flags: snapshot.flags
698
+ }
699
+ if (snapshot.sponsor) {
700
+ effect.sponsor = snapshot.sponsor
701
+ }
702
+ switch (action) {
703
+ case 'created':
704
+ effect.type = effectTypes.offerCreated
705
+ effect.amount = after.amount
706
+ effect.price = after.price
707
+ break
708
+ case 'updated':
709
+ if (before.price === after.price && before.asset.join() === after.asset.join() && before.amount === after.amount)
710
+ return //no changes - skip
711
+ effect.type = effectTypes.offerUpdated
712
+ effect.amount = after.amount
713
+ effect.price = after.price
714
+ break
715
+ }
716
+ this.addEffect(effect)
717
+ }
718
+
719
+ processLiquidityPoolChanges({action, before, after}) {
720
+ const snapshot = after || before
721
+ const effect = {
722
+ type: effectTypes.liquidityPoolRemoved,
723
+ pool: snapshot.pool
724
+ }
725
+ if (snapshot.sponsor) {
726
+ effect.sponsor = snapshot.sponsor
727
+ }
728
+ switch (action) {
729
+ case 'created':
730
+ Object.assign(effect, {
731
+ type: effectTypes.liquidityPoolCreated,
732
+ reserves: after.asset.map(asset => ({asset, amount: '0'})),
733
+ shares: '0',
734
+ accounts: 1
735
+ })
736
+ this.addEffect(effect, this.effects.findIndex(e => e.pool === effect.pool || e.asset === effect.pool))
737
+ return
738
+ case 'updated':
739
+ Object.assign(effect, {
740
+ type: effectTypes.liquidityPoolUpdated,
741
+ reserves: after.asset.map((asset, i) => ({
742
+ asset,
743
+ amount: after.amount[i]
744
+ })),
745
+ shares: after.shares,
746
+ accounts: after.accounts
747
+ })
748
+ break
749
+ }
750
+ this.addEffect(effect)
751
+ }
752
+
753
+ processClaimableBalanceChanges({action, before, after}) {
754
+ switch (action) {
755
+ case 'created':
756
+ this.addEffect({
757
+ type: effectTypes.claimableBalanceCreated,
758
+ sponsor: after.sponsor,
759
+ balance: after.balanceId,
760
+ asset: after.asset,
761
+ amount: after.amount,
762
+ claimants: after.claimants
763
+ })
764
+ break
765
+ case 'removed':
766
+ this.addEffect({
767
+ type: effectTypes.claimableBalanceRemoved,
768
+ sponsor: before.sponsor,
769
+ balance: before.balanceId,
770
+ asset: before.asset,
771
+ amount: before.amount,
772
+ claimants: before.claimants
773
+ })
774
+ break
775
+ case 'updated':
776
+ //nothing to process here
777
+ break
778
+ }
779
+ }
780
+
781
+ processDataEntryChanges({action, before, after}) {
782
+ const effect = {type: ''}
783
+ const {sponsor, name, value} = after || before
784
+ effect.name = name
785
+ effect.value = value && value.toString('base64')
786
+ switch (action) {
787
+ case 'created':
788
+ effect.type = effectTypes.dataEntryCreated
789
+ break
790
+ case 'updated':
791
+ if (before.value === after.value)
792
+ return //value has not changed
793
+ effect.type = effectTypes.dataEntryUpdated
794
+ break
795
+ case 'removed':
796
+ effect.type = effectTypes.dataEntryRemoved
797
+ delete effect.value
798
+ break
799
+ }
800
+ if (sponsor) {
801
+ effect.sponsor = sponsor
802
+ }
803
+ this.addEffect(effect)
804
+ }
805
+
806
+ processContractBalance(effect) {
807
+ const parsedKey = xdr.ScVal.fromXDR(effect.key, 'base64')
808
+ if (parsedKey._arm !== 'vec')
809
+ return
810
+ const keyParts = parsedKey._value
811
+ if (!(keyParts instanceof Array) || keyParts.length !== 2)
812
+ return
813
+ if (keyParts[0]._arm !== 'sym' || keyParts[1]._arm !== 'address' || keyParts[0]._value.toString() !== 'Balance')
814
+ return
815
+ const account = xdrParseScVal(keyParts[1])
816
+ const balanceEffects = this.effects.filter(e => (e.type === effectTypes.accountCredited || e.type === effectTypes.accountDebited) && e.source === account && e.asset === effect.owner)
817
+ if (balanceEffects.length !== 1) //we can set balance only when we found 1-1 mapping, if there are several balance changes, we can't establish balance relation
818
+ return
819
+ if (effect.type === effectTypes.contractDataRemoved) { //balance completely removed - this may be a reversible operation if the balance simply expired
820
+ balanceEffects[0].balance = '0'
821
+ return
822
+ }
823
+ const value = xdr.ScVal.fromXDR(effect.value, 'base64')
824
+ if (value._arm !== 'map')
825
+ return
826
+ const parsedValue = xdrParseScVal(value)
827
+ if (typeof parsedValue.clawback !== 'boolean' || typeof parsedValue.authorized !== 'boolean' || typeof parsedValue.amount !== 'string')
828
+ return
829
+ //set transfer effect balance
830
+ balanceEffects[0].balance = parsedValue.amount
831
+ }
832
+
833
+ processContractChanges({action, before, after}) {
834
+ const {kind, owner: contract, keyHash} = after
835
+ let effect = {
836
+ type: effectTypes.contractCreated,
837
+ contract,
838
+ kind,
839
+ keyHash
840
+ }
841
+ switch (kind) {
842
+ case 'fromAsset':
843
+ effect.asset = after.asset
844
+ break
845
+ case 'wasm':
846
+ effect.wasmHash = after.wasmHash
847
+ break
848
+ default:
849
+ throw new TxMetaEffectParserError('Unexpected contract type: ' + kind)
850
+ }
851
+ switch (action) {
852
+ case 'created':
853
+ if (this.effects.some(e => e.type === effectTypes.contractCreated && e.contract === contract)) {
854
+ effect = undefined //skip contract creation effects processed by top-level createContract operation call
855
+ }
856
+ break
857
+ case 'updated':
858
+ effect.type = effectTypes.contractUpdated
859
+ effect.prevWasmHash = before.wasmHash
860
+ if (before.wasmHash === after.wasmHash) {//skip if hash unchanged
861
+ effect = undefined
862
+ }
863
+ break
864
+ case 'restored':
865
+ effect.type = effectTypes.contractRestored
866
+ break
867
+ default:
868
+ throw new UnexpectedTxMetaChangeError({type: 'contract', action})
869
+ }
870
+ if (effect) {
871
+ this.addEffect(effect, effect.type === effectTypes.contractCreated ? 0 : undefined)
872
+ }
873
+ if (before?.storage?.length || after?.storage?.length) {
874
+ this.processInstanceDataChanges(before, after, action === 'restored')
875
+ }
876
+ }
877
+
878
+ processContractStateEntryChanges({action, before, after}) {
879
+ const {owner, key, durability, keyHash} = after || before
880
+ const effect = {
881
+ type: '',
882
+ owner,
883
+ key,
884
+ durability,
885
+ keyHash
886
+ }
887
+ switch (action) {
888
+ case 'created':
889
+ effect.type = effectTypes.contractDataCreated
890
+ effect.value = after.value
891
+ break
892
+ case 'updated':
893
+ if (before.value === after.value)
894
+ return //value has not changed
895
+ effect.type = effectTypes.contractDataUpdated
896
+ effect.value = after.value
897
+ effect.prevValue = before.value
898
+ break
899
+ case 'removed':
900
+ effect.type = effectTypes.contractDataRemoved
901
+ effect.prevValue = before.value
902
+ break
903
+ case 'restored':
904
+ effect.type = effectTypes.contractDataRestored
905
+ effect.value = after.value
906
+ break
907
+ }
908
+ this.addEffect(effect)
909
+ this.processContractBalance(effect)
910
+ }
911
+
912
+ processContractCodeChanges({type, action, before, after}) {
913
+ const {hash, keyHash} = after || before
914
+ switch (action) {
915
+ case 'created':
916
+ break //processed separately
917
+ case 'updated':
918
+ break //it doesn't change the state
919
+ case 'removed':
920
+ this.addEffect({
921
+ type: effectTypes.contractCodeRemoved,
922
+ wasmHash: hash,
923
+ keyHash
924
+ })
925
+ break
926
+ case 'restored':
927
+ this.addEffect({
928
+ type: effectTypes.contractCodeRestored,
929
+ wasmHash: hash,
930
+ keyHash
931
+ })
932
+ break
933
+ }
934
+ }
935
+
936
+ processInstanceDataChanges(before, after, restored) {
937
+ const storageBefore = before?.storage || []
938
+ const storageAfter = [...(after?.storage || [])]
939
+ if (!restored) {
940
+ for (const {key, val} of storageBefore) {
941
+ let newVal
942
+ for (let i = 0; i < storageAfter.length; i++) {
943
+ const afterValue = storageAfter[i]
944
+ if (afterValue.key === key) {
945
+ newVal = afterValue.val //update new value
946
+ storageAfter.splice(i, 1) //remove from array to simplify iteration
947
+ break
948
+ }
949
+ }
950
+ if (newVal === undefined) { //removed
951
+ const effect = {
952
+ type: effectTypes.contractDataRemoved,
953
+ owner: after?.owner || before.owner,
954
+ key,
955
+ prevValue: val,
956
+ durability: 'instance'
957
+ }
958
+ this.addEffect(effect)
959
+ continue
960
+ }
961
+ if (val === newVal) //value has not changed
962
+ continue
963
+
964
+ const effect = {
965
+ type: effectTypes.contractDataUpdated,
966
+ owner: after?.owner || before.owner,
967
+ key,
968
+ value: newVal,
969
+ prevValue: val,
970
+ durability: 'instance'
971
+ }
972
+ this.addEffect(effect)
973
+ }
974
+ }
975
+ //iterate all storage items left
976
+ for (const {key, val} of storageAfter) {
977
+ const effect = {
978
+ type: restored ? effectTypes.contractDataRestored : effectTypes.contractDataCreated,
979
+ owner: after?.owner || before.owner,
980
+ key,
981
+ value: val,
982
+ durability: 'instance'
983
+ }
984
+ this.addEffect(effect)
985
+ }
986
+ }
987
+
988
+ processTtlChanges({action, before, after}) {
989
+ /*if (action === 'removed')
990
+ throw new UnexpectedTxMetaChangeError({type: 'ttl', action})*/
991
+ const {keyHash, ttl} = after || before
992
+ const stateEffect = this.effects.find(e => e.keyHash === keyHash && e.type !== effectTypes.setTtl)
993
+ const effect = {
994
+ type: effectTypes.setTtl,
995
+ keyHash,
996
+ ttl
997
+ }
998
+ if (stateEffect) {
999
+ if (stateEffect.type.startsWith('contractCode')) {
1000
+ effect.kind = 'contractCode'
1001
+ } else if (stateEffect.type.startsWith('contractData')) {
1002
+ effect.kind = 'contractData'
1003
+ effect.owner = stateEffect.owner
1004
+ } else if (stateEffect.type.startsWith('contract')) {
1005
+ effect.kind = 'contractData'
1006
+ effect.owner = stateEffect.contract
1007
+ } else
1008
+ throw new UnexpectedTxMetaChangeError({type: 'ttl', action: stateEffect.type})
1009
+ stateEffect.ttl = ttl
1010
+ }
1011
+ this.addEffect(effect)
1012
+ }
1013
+
1014
+ processChanges() {
1015
+ for (const change of this.changes)
1016
+ switch (change.type) {
1017
+ case 'account':
1018
+ this.processAccountChanges(change)
1019
+ break
1020
+ case 'trustline':
1021
+ this.processTrustlineEffectsChanges(change)
1022
+ break
1023
+ case 'claimableBalance':
1024
+ this.processClaimableBalanceChanges(change)
1025
+ break
1026
+ case 'offer':
1027
+ this.processOfferChanges(change)
1028
+ break
1029
+ case 'liquidityPool':
1030
+ this.processLiquidityPoolChanges(change)
1031
+ break
1032
+ case 'data':
1033
+ this.processDataEntryChanges(change)
1034
+ break
1035
+ case 'contractData':
1036
+ if (change.before?.kind || change.after?.kind) {
1037
+ this.processContractChanges(change)
1038
+ }
1039
+ break
1040
+ case 'contractCode':
1041
+ this.processContractCodeChanges(change)
1042
+ break
1043
+ case 'ttl':
1044
+ this.processTtlChanges(change)
1045
+ break
1046
+ default:
1047
+ throw new UnexpectedTxMetaChangeError(change)
1048
+ }
1049
+ }
1050
+
1051
+ processStateChanges() {
1052
+ for (const change of this.changes)
1053
+ if (change.type === 'contractData') {
1054
+ this.processContractStateEntryChanges(change)
1055
+ }
1056
+ }
1057
+
1058
+ /**
1059
+ * @return {String|null}
1060
+ * @private
1061
+ */
1062
+ retrieveOpContractId() {
1063
+ const funcValue = this.operation.func._value._attributes
1064
+ if (funcValue) {
1065
+ if (funcValue.contractAddress)
1066
+ return StrKey.encodeContract(funcValue.contractAddress._value)
1067
+ const preimage = funcValue.contractIdPreimage
1068
+ if (preimage)
1069
+ return contractIdFromPreimage(preimage, this.network)
1070
+ }
1071
+ return null
1072
+ }
1073
+
1074
+ /**
1075
+ *
1076
+ * @param assetOrContract
1077
+ * @return {*}
1078
+ */
1079
+ resolveAsset(assetOrContract) {
1080
+ if (!assetOrContract.startsWith('C') || !this.sacMap)
1081
+ return assetOrContract
1082
+ //try to resolve using SAC map
1083
+ return this.sacMap.get(assetOrContract) || assetOrContract
1084
+ }
1085
+ }
1086
+
1087
+ /**
1088
+ * Generates fee charged effect
1089
+ * @param {{}} tx - Transaction
1090
+ * @param {String} source - Source account
1091
+ * @param {String} chargedAmount - Charged amount
1092
+ * @param {Boolean} [feeBump] - Is fee bump transaction
1093
+ * @returns {{}} - Fee charged effect
1094
+ */
1095
+ function processFeeChargedEffect(tx, source, chargedAmount, feeBump = false) {
1096
+ if (tx._switch) { //raw XDR
1097
+ const txXdr = tx.value().tx()
1098
+ tx = {
1099
+ source: xdrParseAccountAddress((txXdr.feeSource ? txXdr.feeSource : txXdr.sourceAccount).call(txXdr)),
1100
+ fee: txXdr.fee().toString()
1101
+ }
1102
+ }
1103
+ const res = {
1104
+ type: effectTypes.feeCharged,
1105
+ source,
1106
+ asset: 'XLM',
1107
+ bid: tx.fee,
1108
+ charged: chargedAmount
1109
+ }
1110
+ if (feeBump) {
1111
+ res.bump = true
1112
+ }
1113
+ return res
1114
+ }
1115
+
1116
+ /**
1117
+ * @param {String} action
1118
+ * @param {String} type
1119
+ * @return {String}
1120
+ */
1121
+ function encodeSponsorshipEffectName(action, type) {
1122
+ let actionKey
1123
+ switch (action) {
1124
+ case 'created':
1125
+ actionKey = 'Created'
1126
+ break
1127
+ case 'updated':
1128
+ actionKey = 'Updated'
1129
+ break
1130
+ case 'removed':
1131
+ actionKey = 'Removed'
1132
+ break
1133
+ case 'restored':
1134
+ actionKey = 'Restored'
1135
+ break
1136
+ default:
1137
+ throw new UnexpectedTxMetaChangeError({action, type})
1138
+ }
1139
+ return effectTypes[`${type}Sponsorship${actionKey}`]
1140
+ }
1141
+
1142
+ module.exports = {EffectsAnalyzer, processFeeChargedEffect}