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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1154 +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 {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}
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}