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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1123 +1,1154 @@
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 {parseLedgerEntryChanges} = require('./parser/ledger-entry-changes-parser')
4
+ const {xdrParseAsset, xdrParseAccountAddress, xdrParseScVal} = require('./parser/tx-xdr-parser-utils')
5
+ const {contractIdFromPreimage} = require('./parser/contract-preimage-encoder')
6
+ const {generateContractCodeEntryHash} = require('./parser/ledger-key')
7
+ const {analyzeSignerChanges} = require('./aggregation/signer-changes-analyzer')
8
+ const EventsAnalyzer = require('./aggregation/events-analyzer')
9
+ const AssetSupplyAnalyzer = require('./aggregation/asset-supply-analyzer')
10
+ const {mapSacContract} = require('./aggregation/sac-contract-mapper')
11
+ const {UnexpectedTxMetaChangeError, TxMetaEffectParserError} = require('./errors')
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
+ accounts: after.accounts
357
+ })
358
+ }
359
+
360
+ liquidityPoolWithdraw() {
361
+ const pool = this.operation.liquidityPoolId
362
+ const change = this.changes.find(ch => ch.type === 'liquidityPool' && ch.action === 'updated' && ch.before.pool === pool)
363
+ if (!change) //tx failed
364
+ return
365
+ const {before, after} = change
366
+ this.addEffect({
367
+ type: effectTypes.liquidityPoolWithdrew,
368
+ pool,
369
+ assets: before.asset.map((asset, i) => ({
370
+ asset,
371
+ amount: (before.amount[i] - after.amount[i]).toString()
372
+ })),
373
+ shares: (before.shares - after.shares).toString(),
374
+ accounts: after.accounts
375
+ })
376
+ }
377
+
378
+ invokeHostFunction() {
379
+ const {func} = this.operation
380
+ const value = func.value()
381
+ switch (func.arm()) {
382
+ case 'invokeContract':
383
+ if (!this.diagnosticEvents) {
384
+ //add top-level contract invocation effect only if diagnostic events are unavailable
385
+ const rawArgs = value.args()
386
+ const effect = {
387
+ type: effectTypes.contractInvoked,
388
+ contract: xdrParseScVal(value.contractAddress()),
389
+ function: value.functionName().toString(),
390
+ args: rawArgs.map(xdrParseScVal),
391
+ rawArgs: nativeToScVal(rawArgs).toXDR('base64')
392
+ }
393
+ this.addEffect(effect)
394
+ }
395
+ break
396
+ case 'wasm': {
397
+ const codeHash = hash(value)
398
+ this.addEffect({
399
+ type: effectTypes.contractCodeUploaded,
400
+ wasm: value.toString('base64'),
401
+ wasmHash: codeHash.toString('hex'),
402
+ keyHash: generateContractCodeEntryHash(codeHash)
403
+ })
404
+ break
405
+ }
406
+ case 'createContract':
407
+ case 'createContractV2':
408
+ const preimage = value.contractIdPreimage()
409
+ const executable = value.executable()
410
+ const executableType = executable.switch().name
411
+
412
+ const effect = {
413
+ type: effectTypes.contractCreated,
414
+ contract: contractIdFromPreimage(preimage, this.network)
415
+ }
416
+ switch (executableType) {
417
+ case 'contractExecutableWasm':
418
+ effect.kind = 'wasm'
419
+ effect.wasmHash = executable.wasmHash().toString('hex')
420
+ break
421
+ case 'contractExecutableStellarAsset':
422
+ const preimageParams = preimage.value()
423
+ switch (preimage.switch().name) {
424
+ case 'contractIdPreimageFromAddress':
425
+ effect.kind = 'fromAddress'
426
+ effect.issuer = xdrParseAccountAddress(preimageParams.address().value())
427
+ effect.salt = preimageParams.salt().toString('base64')
428
+ break
429
+ case 'contractIdPreimageFromAsset':
430
+ effect.kind = 'fromAsset'
431
+ effect.asset = xdrParseAsset(preimageParams)
432
+ break
433
+ default:
434
+ throw new TxMetaEffectParserError('Unknown preimage type: ' + preimage.switch().name)
435
+ }
436
+ break
437
+ default:
438
+ throw new TxMetaEffectParserError('Unknown contract type: ' + executableType)
439
+ }
440
+ if (func.arm() === 'createContractV2') {
441
+ const args = value.constructorArgs() //array
442
+ if (args.length > 0) {
443
+ effect.constructorArgs = args.map(arg => arg.toXDR('base64'))
444
+ }
445
+ }
446
+ this.addEffect(effect, 0)
447
+ break
448
+ default:
449
+ throw new TxMetaEffectParserError('Unknown host function call type: ' + func.arm())
450
+ }
451
+ }
452
+
453
+ bumpFootprintExpiration() {
454
+ //const {ledgersToExpire} = this.operation
455
+ }
456
+
457
+ restoreFootprint() {
458
+ }
459
+
460
+ setAdmin(contractId, newAdmin) {
461
+ const effect = {
462
+ type: effectTypes.contractUpdated,
463
+ contract: contractId,
464
+ admin: newAdmin
465
+ }
466
+ this.addEffect(effect)
467
+ }
468
+
469
+ processDexOperationEffects() {
470
+ if (!this.result)
471
+ return
472
+ //process trades first
473
+ for (const claimedOffer of this.result.claimedOffers) {
474
+ const trade = {
475
+ type: effectTypes.trade,
476
+ amount: claimedOffer.amount,
477
+ asset: claimedOffer.asset
478
+ }
479
+ if (claimedOffer.poolId) {
480
+ trade.pool = claimedOffer.poolId.toString('hex')
481
+ } else {
482
+ trade.offer = claimedOffer.offerId
483
+ trade.seller = claimedOffer.account
484
+
485
+ }
486
+ this.addEffect(trade)
487
+ }
488
+ }
489
+
490
+ processSponsorshipEffects() {
491
+ for (const change of this.changes) {
492
+ const {type, action, before, after} = change
493
+ const effect = {}
494
+ switch (action) {
495
+ case 'created':
496
+ case 'restored':
497
+ if (!after.sponsor)
498
+ continue
499
+ effect.sponsor = after.sponsor
500
+ break
501
+ case 'updated':
502
+ if (before.sponsor === after.sponsor)
503
+ continue
504
+ effect.sponsor = after.sponsor
505
+ effect.prevSponsor = before.sponsor
506
+ break
507
+ case 'removed':
508
+ if (!before.sponsor)
509
+ continue
510
+ effect.prevSponsor = before.sponsor
511
+ break
512
+ }
513
+ switch (type) {
514
+ case 'account':
515
+ effect.account = before?.address || after?.address
516
+ break
517
+ case 'trustline':
518
+ effect.account = before?.account || after?.account
519
+ effect.asset = before?.asset || after?.asset
520
+ break
521
+ case 'offer':
522
+ effect.account = before?.account || after?.account
523
+ effect.offer = before?.id || after?.id
524
+ break
525
+ case 'data':
526
+ effect.account = before?.account || after?.account
527
+ effect.name = before?.name || after?.name
528
+ break
529
+ case 'claimableBalance':
530
+ effect.balance = before?.balanceId || after?.balanceId
531
+ //TODO: add claimable balance asset to the effect
532
+ break
533
+ case 'liquidityPool': //ignore??
534
+ continue
535
+ }
536
+ effect.type = encodeSponsorshipEffectName(action, type)
537
+ this.addEffect(effect)
538
+ }
539
+ }
540
+
541
+ processAccountChanges({action, before, after}) {
542
+ switch (action) {
543
+ case 'created':
544
+ const accountCreated = {
545
+ type: effectTypes.accountCreated,
546
+ account: after.address
547
+ }
548
+ if (after.sponsor) {
549
+ accountCreated.sponsor = after.sponsor
550
+ }
551
+ this.addEffect(accountCreated)
552
+ if (after.balance > 0) {
553
+ this.credit(after.balance, 'XLM', after.address, after.balance)
554
+ }
555
+ break
556
+ case 'updated':
557
+ if (before.balance !== after.balance) {
558
+ this.processBalanceChange(after.address, 'XLM', before.balance, after.balance)
559
+ }
560
+ //other operations do not yield signer sponsorship effects
561
+ if (this.operation.type === 'setOptions' || this.operation.type === 'revokeSignerSponsorship') {
562
+ this.processSignerSponsorshipEffects({before, after})
563
+ }
564
+ break
565
+ case 'removed':
566
+ if (before.balance > 0) {
567
+ this.debit(before.balance, 'XLM', before.address, '0')
568
+ }
569
+ const accountRemoved = {
570
+ type: effectTypes.accountRemoved
571
+ }
572
+ if (before.sponsor) {
573
+ accountRemoved.sponsor = before.sponsor
574
+ }
575
+ this.addEffect(accountRemoved)
576
+ break
577
+ }
578
+
579
+ for (const effect of analyzeSignerChanges(before, after)) {
580
+ this.addEffect(effect)
581
+ }
582
+ }
583
+
584
+ processTrustlineEffectsChanges({action, before, after}) {
585
+ const snapshot = (after || before)
586
+ const trustEffect = {
587
+ type: '',
588
+ source: snapshot.account,
589
+ asset: snapshot.asset,
590
+ kind: snapshot.asset.includes('-') ? 'asset' : 'poolShares',
591
+ flags: snapshot.flags
592
+ }
593
+ if (snapshot.sponsor) {
594
+ trustEffect.sponsor = snapshot.sponsor
595
+ }
596
+ switch (action) {
597
+ case 'created':
598
+ trustEffect.type = effectTypes.trustlineCreated
599
+ trustEffect.limit = snapshot.limit
600
+ break
601
+ case 'updated':
602
+ if (before.balance !== after.balance) {
603
+ this.processBalanceChange(after.account, after.asset, before.balance, after.balance)
604
+ }
605
+ if (before.limit === after.limit && before.flags === after.flags)
606
+ return
607
+ trustEffect.type = effectTypes.trustlineUpdated
608
+ trustEffect.limit = snapshot.limit
609
+ trustEffect.prevFlags = before.flags
610
+ break
611
+ case 'removed':
612
+ trustEffect.type = effectTypes.trustlineRemoved
613
+ if (before.balance > 0) {
614
+ this.processBalanceChange(before.account, before.asset, before.balance, '0')
615
+ }
616
+ break
617
+ }
618
+ this.addEffect(trustEffect)
619
+ }
620
+
621
+ processBalanceChange(account, asset, beforeBalance, afterBalance) {
622
+ if (this.isContractCall) { //map contract=>asset proactively
623
+ mapSacContract(this, undefined, asset)
624
+ }
625
+ const balanceChange = BigInt(afterBalance) - BigInt(beforeBalance)
626
+ if (balanceChange < 0n) {
627
+ this.debit((-balanceChange).toString(), asset, account, afterBalance)
628
+ } else {
629
+ this.credit(balanceChange.toString(), asset, account, afterBalance)
630
+ }
631
+ }
632
+
633
+ processSignerSponsorshipEffects({before, after}) {
634
+ if (!before.signerSponsoringIDs?.length && !after.signerSponsoringIDs?.length)
635
+ return
636
+ const [beforeMap, afterMap] = [before, after].map(state => {
637
+ const signersMap = {}
638
+ if (state.signerSponsoringIDs?.length) {
639
+ for (let i = 0; i < state.signers.length; i++) {
640
+ const sponsor = state.signerSponsoringIDs[i]
641
+ if (sponsor) { //add only sponsored signers to the map
642
+ signersMap[state.signers[i].key] = sponsor
643
+ }
644
+ }
645
+ }
646
+ return signersMap
647
+ })
648
+
649
+ for (const signerKey of Object.keys(beforeMap)) {
650
+ const newSponsor = afterMap[signerKey]
651
+ if (!newSponsor) {
652
+ this.addEffect({
653
+ type: effectTypes.signerSponsorshipRemoved,
654
+ account: before.address,
655
+ signer: signerKey,
656
+ prevSponsor: beforeMap[signerKey]
657
+ })
658
+ break
659
+ }
660
+ if (newSponsor !== beforeMap[signerKey]) {
661
+ this.addEffect({
662
+ type: effectTypes.signerSponsorshipUpdated,
663
+ account: before.address,
664
+ signer: signerKey,
665
+ sponsor: newSponsor,
666
+ prevSponsor: beforeMap[signerKey]
667
+ })
668
+ break
669
+ }
670
+ }
671
+
672
+ for (const signerKey of Object.keys(afterMap)) {
673
+ const prevSponsor = beforeMap[signerKey]
674
+ if (!prevSponsor) {
675
+ this.addEffect({
676
+ type: effectTypes.signerSponsorshipCreated,
677
+ account: after.address,
678
+ signer: signerKey,
679
+ sponsor: afterMap[signerKey]
680
+ })
681
+ break
682
+ }
683
+ }
684
+ }
685
+
686
+ processOfferChanges({action, before, after}) {
687
+ const snapshot = after || before
688
+ const effect = {
689
+ type: effectTypes.offerRemoved,
690
+ owner: snapshot.account,
691
+ offer: snapshot.id,
692
+ asset: snapshot.asset,
693
+ flags: snapshot.flags
694
+ }
695
+ if (snapshot.sponsor) {
696
+ effect.sponsor = snapshot.sponsor
697
+ }
698
+ switch (action) {
699
+ case 'created':
700
+ effect.type = effectTypes.offerCreated
701
+ effect.amount = after.amount
702
+ effect.price = after.price
703
+ break
704
+ case 'updated':
705
+ if (before.price === after.price && before.asset.join() === after.asset.join() && before.amount === after.amount)
706
+ return //no changes - skip
707
+ effect.type = effectTypes.offerUpdated
708
+ effect.amount = after.amount
709
+ effect.price = after.price
710
+ break
711
+ }
712
+ this.addEffect(effect)
713
+ }
714
+
715
+ processLiquidityPoolChanges({action, before, after}) {
716
+ const snapshot = after || before
717
+ const effect = {
718
+ type: effectTypes.liquidityPoolRemoved,
719
+ pool: snapshot.pool
720
+ }
721
+ if (snapshot.sponsor) {
722
+ effect.sponsor = snapshot.sponsor
723
+ }
724
+ switch (action) {
725
+ case 'created':
726
+ Object.assign(effect, {
727
+ type: effectTypes.liquidityPoolCreated,
728
+ reserves: after.asset.map(asset => ({asset, amount: '0'})),
729
+ shares: '0',
730
+ accounts: 1
731
+ })
732
+ this.addEffect(effect, this.effects.findIndex(e => e.pool === effect.pool || e.asset === effect.pool))
733
+ return
734
+ case 'updated':
735
+ Object.assign(effect, {
736
+ type: effectTypes.liquidityPoolUpdated,
737
+ reserves: after.asset.map((asset, i) => ({
738
+ asset,
739
+ amount: after.amount[i]
740
+ })),
741
+ shares: after.shares,
742
+ accounts: after.accounts
743
+ })
744
+ break
745
+ }
746
+ this.addEffect(effect)
747
+ }
748
+
749
+ processClaimableBalanceChanges({action, before, after}) {
750
+ switch (action) {
751
+ case 'created':
752
+ this.addEffect({
753
+ type: effectTypes.claimableBalanceCreated,
754
+ sponsor: after.sponsor,
755
+ balance: after.balanceId,
756
+ asset: after.asset,
757
+ amount: after.amount,
758
+ claimants: after.claimants
759
+ })
760
+ break
761
+ case 'removed':
762
+ this.addEffect({
763
+ type: effectTypes.claimableBalanceRemoved,
764
+ sponsor: before.sponsor,
765
+ balance: before.balanceId,
766
+ asset: before.asset,
767
+ amount: before.amount,
768
+ claimants: before.claimants
769
+ })
770
+ break
771
+ case 'updated':
772
+ //nothing to process here
773
+ break
774
+ }
775
+ }
776
+
777
+ processDataEntryChanges({action, before, after}) {
778
+ const effect = {type: ''}
779
+ const {sponsor, name, value} = after || before
780
+ effect.name = name
781
+ effect.value = value && value.toString('base64')
782
+ switch (action) {
783
+ case 'created':
784
+ effect.type = effectTypes.dataEntryCreated
785
+ break
786
+ case 'updated':
787
+ if (before.value === after.value)
788
+ return //value has not changed
789
+ effect.type = effectTypes.dataEntryUpdated
790
+ break
791
+ case 'removed':
792
+ effect.type = effectTypes.dataEntryRemoved
793
+ delete effect.value
794
+ break
795
+ }
796
+ if (sponsor) {
797
+ effect.sponsor = sponsor
798
+ }
799
+ this.addEffect(effect)
800
+ }
801
+
802
+ processContractBalance(effect) {
803
+ const parsedKey = xdr.ScVal.fromXDR(effect.key, 'base64')
804
+ if (parsedKey._arm !== 'vec')
805
+ return
806
+ const keyParts = parsedKey._value
807
+ if (!(keyParts instanceof Array) || keyParts.length !== 2)
808
+ return
809
+ if (keyParts[0]._arm !== 'sym' || keyParts[1]._arm !== 'address' || keyParts[0]._value.toString() !== 'Balance')
810
+ return
811
+ const account = xdrParseScVal(keyParts[1])
812
+ const balanceEffects = this.effects.filter(e => (e.type === effectTypes.accountCredited || e.type === effectTypes.accountDebited) && e.source === account && e.asset === effect.owner)
813
+ 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
814
+ return
815
+ if (effect.type === effectTypes.contractDataRemoved) { //balance completely removed - this may be a reversible operation if the balance simply expired
816
+ balanceEffects[0].balance = '0'
817
+ return
818
+ }
819
+ const value = xdr.ScVal.fromXDR(effect.value, 'base64')
820
+ if (value._arm !== 'map')
821
+ return
822
+ const parsedValue = xdrParseScVal(value)
823
+ if (typeof parsedValue.clawback !== 'boolean' || typeof parsedValue.authorized !== 'boolean' || typeof parsedValue.amount !== 'string')
824
+ return
825
+ //set transfer effect balance
826
+ balanceEffects[0].balance = parsedValue.amount
827
+ }
828
+
829
+ processContractChanges({action, before, after}) {
830
+ const {kind, owner: contract, keyHash} = after
831
+ let effect = {
832
+ type: effectTypes.contractCreated,
833
+ contract,
834
+ kind,
835
+ keyHash
836
+ }
837
+ switch (kind) {
838
+ case 'fromAsset':
839
+ effect.asset = after.asset
840
+ break
841
+ case 'wasm':
842
+ effect.wasmHash = after.wasmHash
843
+ break
844
+ default:
845
+ throw new TxMetaEffectParserError('Unexpected contract type: ' + kind)
846
+ }
847
+ switch (action) {
848
+ case 'created':
849
+ if (this.effects.some(e => e.type === effectTypes.contractCreated && e.contract === contract)) {
850
+ effect = undefined //skip contract creation effects processed by top-level createContract operation call
851
+ }
852
+ break
853
+ case 'updated':
854
+ effect.type = effectTypes.contractUpdated
855
+ effect.prevWasmHash = before.wasmHash
856
+ if (before.wasmHash === after.wasmHash) {//skip if hash unchanged
857
+ effect = undefined
858
+ }
859
+ break
860
+ case 'restored':
861
+ effect.type = effectTypes.contractRestored
862
+ break
863
+ default:
864
+ throw new UnexpectedTxMetaChangeError({type: 'contract', action})
865
+ }
866
+ if (effect) {
867
+ this.addEffect(effect, effect.type === effectTypes.contractCreated ? 0 : undefined)
868
+ }
869
+ if (before?.storage?.length || after?.storage?.length) {
870
+ this.processInstanceDataChanges(before, after, action === 'restored')
871
+ }
872
+ }
873
+
874
+ processContractStateEntryChanges({action, before, after}) {
875
+ const {owner, key, durability, keyHash} = after || before
876
+ const effect = {
877
+ type: '',
878
+ owner,
879
+ key,
880
+ durability,
881
+ keyHash
882
+ }
883
+ switch (action) {
884
+ case 'created':
885
+ effect.type = effectTypes.contractDataCreated
886
+ effect.value = after.value
887
+ break
888
+ case 'updated':
889
+ if (before.value === after.value)
890
+ return //value has not changed
891
+ effect.type = effectTypes.contractDataUpdated
892
+ effect.value = after.value
893
+ effect.prevValue = before.value
894
+ break
895
+ case 'removed':
896
+ effect.type = effectTypes.contractDataRemoved
897
+ effect.prevValue = before.value
898
+ break
899
+ case 'restored':
900
+ effect.type = effectTypes.contractDataRestored
901
+ effect.value = after.value
902
+ break
903
+ }
904
+ this.addEffect(effect)
905
+ this.processContractBalance(effect)
906
+ }
907
+
908
+ processContractCodeChanges({type, action, before, after}) {
909
+ const {hash, keyHash} = after || before
910
+ switch (action) {
911
+ case 'created':
912
+ break //processed separately
913
+ case 'updated':
914
+ break //it doesn't change the state
915
+ case 'removed':
916
+ this.addEffect({
917
+ type: effectTypes.contractCodeRemoved,
918
+ wasmHash: hash,
919
+ keyHash
920
+ })
921
+ break
922
+ case 'restored':
923
+ this.addEffect({
924
+ type: effectTypes.contractCodeRestored,
925
+ wasmHash: hash,
926
+ keyHash
927
+ })
928
+ break
929
+ }
930
+ }
931
+
932
+ processInstanceDataChanges(before, after, restored) {
933
+ const storageBefore = before?.storage || []
934
+ const storageAfter = [...(after?.storage || [])]
935
+ if (!restored) {
936
+ for (const {key, val} of storageBefore) {
937
+ let newVal
938
+ for (let i = 0; i < storageAfter.length; i++) {
939
+ const afterValue = storageAfter[i]
940
+ if (afterValue.key === key) {
941
+ newVal = afterValue.val //update new value
942
+ storageAfter.splice(i, 1) //remove from array to simplify iteration
943
+ break
944
+ }
945
+ }
946
+ if (newVal === undefined) { //removed
947
+ const effect = {
948
+ type: effectTypes.contractDataRemoved,
949
+ owner: after?.owner || before.owner,
950
+ key,
951
+ prevValue: val,
952
+ durability: 'instance'
953
+ }
954
+ this.addEffect(effect)
955
+ continue
956
+ }
957
+ if (val === newVal) //value has not changed
958
+ continue
959
+
960
+ const effect = {
961
+ type: effectTypes.contractDataUpdated,
962
+ owner: after?.owner || before.owner,
963
+ key,
964
+ value: newVal,
965
+ prevValue: val,
966
+ durability: 'instance'
967
+ }
968
+ this.addEffect(effect)
969
+ }
970
+ }
971
+ //iterate all storage items left
972
+ for (const {key, val} of storageAfter) {
973
+ const effect = {
974
+ type: restored ? effectTypes.contractDataRestored : effectTypes.contractDataCreated,
975
+ owner: after?.owner || before.owner,
976
+ key,
977
+ value: val,
978
+ durability: 'instance'
979
+ }
980
+ this.addEffect(effect)
981
+ }
982
+ }
983
+
984
+ processTtlChanges({action, before, after}) {
985
+ /*if (action === 'removed')
986
+ throw new UnexpectedTxMetaChangeError({type: 'ttl', action})*/
987
+ const {keyHash, ttl} = after || before
988
+ const stateEffect = this.effects.find(e => e.keyHash === keyHash && e.type !== effectTypes.setTtl)
989
+ const effect = {
990
+ type: effectTypes.setTtl,
991
+ keyHash,
992
+ ttl
993
+ }
994
+ if (stateEffect) {
995
+ if (stateEffect.type.startsWith('contractCode')) {
996
+ effect.kind = 'contractCode'
997
+ } else if (stateEffect.type.startsWith('contractData')) {
998
+ effect.kind = 'contractData'
999
+ effect.owner = stateEffect.owner
1000
+ } else if (stateEffect.type.startsWith('contract')) {
1001
+ effect.kind = 'contractData'
1002
+ effect.owner = stateEffect.contract
1003
+ } else
1004
+ throw new UnexpectedTxMetaChangeError({type: 'ttl', action: stateEffect.type})
1005
+ stateEffect.ttl = ttl
1006
+ }
1007
+ this.addEffect(effect)
1008
+ }
1009
+
1010
+ processChanges() {
1011
+ for (const change of this.changes)
1012
+ switch (change.type) {
1013
+ case 'account':
1014
+ this.processAccountChanges(change)
1015
+ break
1016
+ case 'trustline':
1017
+ this.processTrustlineEffectsChanges(change)
1018
+ break
1019
+ case 'claimableBalance':
1020
+ this.processClaimableBalanceChanges(change)
1021
+ break
1022
+ case 'offer':
1023
+ this.processOfferChanges(change)
1024
+ break
1025
+ case 'liquidityPool':
1026
+ this.processLiquidityPoolChanges(change)
1027
+ break
1028
+ case 'data':
1029
+ this.processDataEntryChanges(change)
1030
+ break
1031
+ case 'contractData':
1032
+ if (change.before?.kind || change.after?.kind) {
1033
+ this.processContractChanges(change)
1034
+ }
1035
+ break
1036
+ case 'contractCode':
1037
+ this.processContractCodeChanges(change)
1038
+ break
1039
+ case 'ttl':
1040
+ this.processTtlChanges(change)
1041
+ break
1042
+ default:
1043
+ throw new UnexpectedTxMetaChangeError(change)
1044
+ }
1045
+ }
1046
+
1047
+ processStateChanges() {
1048
+ for (const change of this.changes)
1049
+ if (change.type === 'contractData') {
1050
+ this.processContractStateEntryChanges(change)
1051
+ }
1052
+ }
1053
+
1054
+ retrieveOpContractId() {
1055
+ const funcValue = this.operation.func._value._attributes
1056
+ if (funcValue) {
1057
+ if (funcValue.contractAddress)
1058
+ return StrKey.encodeContract(funcValue.contractAddress._value)
1059
+ const preimage = funcValue.contractIdPreimage
1060
+ if (preimage)
1061
+ return contractIdFromPreimage(preimage, this.network)
1062
+ }
1063
+ return null
1064
+ }
1065
+
1066
+ resolveAsset(assetOrContract) {
1067
+ if (!assetOrContract.startsWith('C') || !this.sacMap)
1068
+ return assetOrContract
1069
+ //try to resolve using SAC map
1070
+ return this.sacMap[assetOrContract] || assetOrContract
1071
+ }
1072
+ }
1073
+
1074
+ /**
1075
+ * Generates fee charged effect
1076
+ * @param {{}} tx - Transaction
1077
+ * @param {String} source - Source account
1078
+ * @param {String} chargedAmount - Charged amount
1079
+ * @param {Boolean} [feeBump] - Is fee bump transaction
1080
+ * @returns {{}} - Fee charged effect
1081
+ */
1082
+ function processFeeChargedEffect(tx, source, chargedAmount, feeBump = false) {
1083
+ if (tx._switch) { //raw XDR
1084
+ const txXdr = tx.value().tx()
1085
+ tx = {
1086
+ source: xdrParseAccountAddress((txXdr.feeSource ? txXdr.feeSource : txXdr.sourceAccount).call(txXdr)),
1087
+ fee: txXdr.fee().toString()
1088
+ }
1089
+ }
1090
+ const res = {
1091
+ type: effectTypes.feeCharged,
1092
+ source,
1093
+ asset: 'XLM',
1094
+ bid: tx.fee,
1095
+ charged: chargedAmount
1096
+ }
1097
+ if (feeBump) {
1098
+ res.bump = true
1099
+ }
1100
+ return res
1101
+ }
1102
+
1103
+ function normalizeAddress(address) {
1104
+ const prefix = address[0]
1105
+ if (prefix === 'G')
1106
+ return address
1107
+ if (prefix !== 'M')
1108
+ throw new TypeError('Expected ED25519 or Muxed address')
1109
+ const rawBytes = StrKey.decodeMed25519PublicKey(address)
1110
+ return StrKey.encodeEd25519PublicKey(rawBytes.subarray(0, 32))
1111
+ }
1112
+
1113
+
1114
+ /**
1115
+ * @param {String} action
1116
+ * @param {String} type
1117
+ * @return {String}
1118
+ */
1119
+ function encodeSponsorshipEffectName(action, type) {
1120
+ let actionKey
1121
+ switch (action) {
1122
+ case 'created':
1123
+ actionKey = 'Created'
1124
+ break
1125
+ case 'updated':
1126
+ actionKey = 'Updated'
1127
+ break
1128
+ case 'removed':
1129
+ actionKey = 'Removed'
1130
+ break
1131
+ case 'restored':
1132
+ actionKey = 'Restored'
1133
+ break
1134
+ default:
1135
+ throw new UnexpectedTxMetaChangeError({action, type})
1136
+ }
1137
+ return effectTypes[`${type}Sponsorship${actionKey}`]
1138
+ }
1139
+
1140
+ function validateAmount(amount) {
1141
+ if (amount < 0)
1142
+ throw new TxMetaEffectParserError('Negative balance change amount: ' + amount.toString())
1143
+ return amount
1144
+ }
1145
+
1146
+ /**
1147
+ * @param largeInt
1148
+ * @return {String}
1149
+ */
1150
+ function parseLargeInt(largeInt) {
1151
+ return largeInt._value.toString()
1152
+ }
1153
+
1154
+ module.exports = {EffectsAnalyzer, processFeeChargedEffect}