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