@stellar-expert/tx-meta-effects-parser 5.0.0-beta10

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.
@@ -0,0 +1,826 @@
1
+ const {StrKey, hash, xdr, nativeToScVal} = require('@stellar/stellar-base')
2
+ const effectTypes = require('./effect-types')
3
+ const {parseLedgerEntryChanges} = require('./ledger-entry-changes-parser')
4
+ const {xdrParseAsset, xdrParseAccountAddress, xdrParseScVal} = require('./tx-xdr-parser-utils')
5
+ const {encodeSponsorshipEffectName} = require('./analyzer-primitives')
6
+ const {analyzeSignerChanges} = require('./signer-changes-analyzer')
7
+ const {contractIdFromPreimage} = require('./contract-preimage-encoder')
8
+ const EventsAnalyzer = require('./events-analyzer')
9
+ const AssetSupplyProcessor = require('./asset-supply-processor')
10
+ const {UnexpectedTxMetaChangeError, TxMetaEffectParserError} = require('./errors')
11
+
12
+ class EffectsAnalyzer {
13
+ constructor({operation, meta, result, network, events, diagnosticEvents}) {
14
+ //set execution context
15
+ if (!operation.source)
16
+ throw new TxMetaEffectParserError('Operation source is not explicitly defined')
17
+ this.operation = operation
18
+ this.result = result
19
+ this.changes = parseLedgerEntryChanges(meta)
20
+ this.source = this.operation.source
21
+ this.events = events
22
+ if (diagnosticEvents?.length) {
23
+ this.diagnosticEvents = diagnosticEvents
24
+ }
25
+ this.network = network
26
+ }
27
+
28
+ /**
29
+ * @type {{}[]}
30
+ * @private
31
+ * @readonly
32
+ */
33
+ effects = []
34
+ /**
35
+ * @type {Object}
36
+ * @private
37
+ * @readonly
38
+ */
39
+ operation = null
40
+ /**
41
+ * @type {String}
42
+ * @readonly
43
+ */
44
+ network
45
+ /**
46
+ * @type {ParsedLedgerEntryMeta[]}
47
+ * @private
48
+ * @readonly
49
+ */
50
+ changes = null
51
+ /**
52
+ * @type {Object}
53
+ * @private
54
+ * @readonly
55
+ */
56
+ result = null
57
+ /**
58
+ * @type {String}
59
+ * @private
60
+ * @readonly
61
+ */
62
+ source = ''
63
+
64
+ analyze() {
65
+ //find appropriate parsing method
66
+ const parse = this[this.operation.type]
67
+ if (parse) {
68
+ parse.call(this)
69
+ }
70
+ //process ledger entry changes
71
+ this.processChanges()
72
+ //handle effects that are processed indirectly
73
+ this.processSponsorshipEffects()
74
+ //process Soroban events
75
+ new EventsAnalyzer(this).analyze()
76
+ //calculate minted/burned assets
77
+ this.processAssetSupplyEffects()
78
+ //process state data changes in the end
79
+ for (const change of this.changes)
80
+ if (change.type === 'contractData') {
81
+ this.processContractDataChanges(change)
82
+ }
83
+
84
+ return this.effects
85
+ }
86
+
87
+ /**
88
+ * @param {{}} effect
89
+ * @param {Number} [atPosition]
90
+ */
91
+ addEffect(effect, atPosition) {
92
+ if (!effect.source) {
93
+ effect.source = this.source
94
+ }
95
+ if (atPosition !== undefined) {
96
+ this.effects.splice(atPosition < 0 ? 0 : atPosition, 0, effect)
97
+ } else {
98
+ this.effects.push(effect)
99
+ }
100
+ }
101
+
102
+ debit(amount, asset, source, balance) {
103
+ if (amount === '0')
104
+ return
105
+ const effect = {
106
+ type: effectTypes.accountDebited,
107
+ source,
108
+ asset,
109
+ amount
110
+ }
111
+ if (balance !== undefined) {
112
+ effect.balance = balance
113
+ }
114
+ this.addEffect(effect)
115
+ }
116
+
117
+ credit(amount, asset, source, balance) {
118
+ if (amount === '0')
119
+ return
120
+ const effect = {
121
+ type: effectTypes.accountCredited,
122
+ source,
123
+ asset,
124
+ amount
125
+ }
126
+ if (balance !== undefined) {
127
+ effect.balance = balance
128
+ }
129
+ this.addEffect(effect)
130
+ }
131
+
132
+ setOptions() {
133
+ const sourceAccount = normalizeAddress(this.source)
134
+ const {before, after} = this.changes.find(ch => ch.type === 'account' && ch.before.address === sourceAccount)
135
+ if (before.homeDomain !== after.homeDomain) {
136
+ this.addEffect({
137
+ type: effectTypes.accountHomeDomainUpdated,
138
+ domain: after.homeDomain
139
+ })
140
+ }
141
+ if (before.thresholds !== after.thresholds) {
142
+ this.addEffect({
143
+ type: effectTypes.accountThresholdsUpdated,
144
+ thresholds: after.thresholds.split(',').map(v => parseInt(v, 10))
145
+ })
146
+ }
147
+ if (before.flags !== after.flags) {
148
+ this.addEffect({
149
+ type: effectTypes.accountFlagsUpdated,
150
+ flags: after.flags,
151
+ prevFlags: before.flags
152
+ })
153
+ }
154
+ if (before.inflationDest !== after.inflationDest) {
155
+ this.addEffect({
156
+ type: effectTypes.accountInflationDestinationUpdated,
157
+ inflationDestination: after.inflationDest
158
+ })
159
+ }
160
+ }
161
+
162
+ allowTrust() {
163
+ this.setTrustLineFlags()
164
+ }
165
+
166
+ setTrustLineFlags() {
167
+ if (!this.changes.length)
168
+ return
169
+ const trustAsset = xdrParseAsset(this.operation.asset || {code: this.operation.assetCode, issuer: normalizeAddress(this.source)})
170
+ const trustlineChange = this.changes.find(ch => ch.type === 'trustline' && ch.before.asset === trustAsset)
171
+ if (trustlineChange) {
172
+ if (trustlineChange.action !== 'updated')
173
+ throw new UnexpectedTxMetaChangeError(trustlineChange)
174
+ const {before, after} = trustlineChange
175
+ if (before.flags !== after.flags) {
176
+ this.addEffect({
177
+ type: effectTypes.trustlineAuthorizationUpdated,
178
+ trustor: this.operation.trustor,
179
+ asset: after.asset,
180
+ flags: after.flags,
181
+ prevFlags: before.flags
182
+ })
183
+ for (const change of this.changes) {
184
+ if (change.type !== 'liquidityPool')
185
+ continue
186
+ const {before, after} = change
187
+ this.addEffect({
188
+ type: effectTypes.liquidityPoolWithdrew,
189
+ source: this.operation.trustor,
190
+ pool: before.pool,
191
+ assets: before.asset.map((asset, i) => ({
192
+ asset,
193
+ amount: (BigInt(before.amount[i]) - (after ? BigInt(after.amount[i]) : 0n)).toString()
194
+ })),
195
+ shares: (BigInt(before.shares) - (after ? BigInt(after.shares) : 0n)).toString()
196
+ })
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ inflation() {
203
+ /*const paymentEffects = (result.inflationPayouts || []).map(ip => ({
204
+ type: effectTypes.accountCredited,
205
+ source: ip.account,
206
+ asset: 'XLM',
207
+ amount: ip.amount
208
+ }))*/
209
+ this.addEffect({type: effectTypes.inflation})
210
+ }
211
+
212
+ bumpSequence() {
213
+ if (!this.changes.length)
214
+ return
215
+ const {before, after} = this.changes.find(ch => ch.type === 'account')
216
+ if (before.sequence !== after.sequence) {
217
+ this.addEffect({
218
+ type: effectTypes.sequenceBumped,
219
+ sequence: after.sequence
220
+ })
221
+ }
222
+ }
223
+
224
+ pathPaymentStrictReceive() {
225
+ this.processDexOperationEffects()
226
+ }
227
+
228
+ pathPaymentStrictSend() {
229
+ this.processDexOperationEffects()
230
+ }
231
+
232
+ manageSellOffer() {
233
+ this.processDexOperationEffects()
234
+ }
235
+
236
+ manageBuyOffer() {
237
+ this.processDexOperationEffects()
238
+ }
239
+
240
+ createPassiveSellOffer() {
241
+ this.processDexOperationEffects()
242
+ }
243
+
244
+ liquidityPoolDeposit() {
245
+ const {liquidityPoolId} = this.operation
246
+ const {
247
+ before,
248
+ after
249
+ } = this.changes.find(ch => ch.type === 'liquidityPool' && ch.action === 'updated' && ch.after.pool === liquidityPoolId)
250
+ this.addEffect({
251
+ type: effectTypes.liquidityPoolDeposited,
252
+ pool: this.operation.liquidityPoolId,
253
+ assets: after.asset.map((asset, i) => ({
254
+ asset,
255
+ amount: (after.amount[i] - before.amount[i]).toString()
256
+ })),
257
+ shares: (after.shares - before.shares).toString()
258
+ })
259
+ }
260
+
261
+ liquidityPoolWithdraw() {
262
+ const pool = this.operation.liquidityPoolId
263
+ const {before, after} = this.changes.find(ch => ch.type === 'liquidityPool' && ch.action === 'updated' && ch.before.pool === pool)
264
+ this.addEffect({
265
+ type: effectTypes.liquidityPoolWithdrew,
266
+ pool,
267
+ assets: before.asset.map((asset, i) => ({
268
+ asset,
269
+ amount: (before.amount[i] - after.amount[i]).toString()
270
+ })),
271
+ shares: (before.shares - after.shares).toString()
272
+ })
273
+ }
274
+
275
+ invokeHostFunction() {
276
+ const {func} = this.operation
277
+ const value = func.value()
278
+ switch (func.arm()) {
279
+ case 'invokeContract':
280
+ if (!this.diagnosticEvents) {
281
+ //add top-level contract invocation effect only if diagnostic events are unavailable
282
+ const rawArgs = value.args()
283
+ const effect = {
284
+ type: effectTypes.contractInvoked,
285
+ contract: xdrParseScVal(value.contractAddress()),
286
+ function: value.functionName().toString(),
287
+ args: rawArgs.map(xdrParseScVal),
288
+ rawArgs: nativeToScVal(rawArgs).toXDR('base64')
289
+ }
290
+ this.addEffect(effect)
291
+ }
292
+ break
293
+ case 'wasm':
294
+ this.addEffect({
295
+ type: effectTypes.contractCodeUploaded,
296
+ wasm: value.toString('base64'),
297
+ wasmHash: hash(value).toString('hex')
298
+ })
299
+ break
300
+ case 'createContract':
301
+ const preimage = value.contractIdPreimage()
302
+ const executable = value.executable()
303
+ const executableType = executable.switch().name
304
+
305
+ const effect = {
306
+ type: effectTypes.contractCreated,
307
+ contract: contractIdFromPreimage(preimage, this.network)
308
+ }
309
+ switch (executableType) {
310
+ case 'contractExecutableWasm':
311
+ effect.kind = 'wasm'
312
+ effect.wasmHash = executable.wasmHash().toString('hex')
313
+ break
314
+ case 'contractExecutableStellarAsset':
315
+ const preimageParams = preimage.value()
316
+ switch (preimage.switch().name) {
317
+ case 'contractIdPreimageFromAddress':
318
+ effect.kind = 'fromAddress'
319
+ effect.issuer = xdrParseAccountAddress(preimageParams.address().value())
320
+ effect.salt = preimageParams.salt().toString('base64')
321
+ break
322
+ case 'contractIdPreimageFromAsset':
323
+ effect.kind = 'fromAsset'
324
+ effect.asset = xdrParseAsset(preimageParams)
325
+ break
326
+ default:
327
+ throw new TxMetaEffectParserError('Unknown preimage type: ' + preimage.switch().name)
328
+ }
329
+ break
330
+ default:
331
+ throw new TxMetaEffectParserError('Unknown contract type: ' + executableType)
332
+ }
333
+ this.addEffect(effect)
334
+ break
335
+ default:
336
+ throw new TxMetaEffectParserError('Unknown host function call type: ' + func.arm())
337
+ }
338
+ }
339
+
340
+ bumpFootprintExpiration() {
341
+ //const {ledgersToExpire} = this.operation
342
+ }
343
+
344
+ restoreFootprint() {
345
+ }
346
+
347
+ processDexOperationEffects() {
348
+ //process trades first
349
+ for (const claimedOffer of this.result.claimedOffers) {
350
+ const trade = {
351
+ type: effectTypes.trade,
352
+ amount: claimedOffer.amount,
353
+ asset: claimedOffer.asset
354
+ }
355
+ if (claimedOffer.poolId) {
356
+ trade.pool = claimedOffer.poolId.toString('hex')
357
+ } else {
358
+ trade.offer = claimedOffer.offerId
359
+ trade.seller = claimedOffer.account
360
+
361
+ }
362
+ this.addEffect(trade)
363
+ }
364
+ }
365
+
366
+ processSponsorshipEffects() {
367
+ for (const change of this.changes) {
368
+ const {type, action, before, after} = change
369
+ const effect = {}
370
+ switch (action) {
371
+ case 'created':
372
+ if (!after.sponsor)
373
+ continue
374
+ effect.sponsor = after.sponsor
375
+ break
376
+ case 'updated':
377
+ if (before.sponsor === after.sponsor)
378
+ continue
379
+ effect.sponsor = after.sponsor
380
+ effect.prevSponsor = before.sponsor
381
+ break
382
+ case 'removed':
383
+ if (!before.sponsor)
384
+ continue
385
+ effect.prevSponsor = before.sponsor
386
+ break
387
+ }
388
+ switch (type) {
389
+ case 'account':
390
+ effect.account = before?.address || after?.address
391
+ break
392
+ case 'trustline':
393
+ effect.account = before?.account || after?.account
394
+ effect.asset = before?.asset || after?.asset
395
+ break
396
+ case 'offer':
397
+ effect.account = before?.account || after?.account
398
+ effect.offer = before?.id || after?.id
399
+ break
400
+ case 'data':
401
+ effect.account = before?.account || after?.account
402
+ effect.name = before?.name || after?.name
403
+ break
404
+ case 'claimableBalance':
405
+ effect.balance = before?.balanceId || after?.balanceId
406
+ //TODO: add claimable balance asset to the effect
407
+ break
408
+ case 'liquidityPool': //ignore??
409
+ continue
410
+ }
411
+ effect.type = encodeSponsorshipEffectName(action, type)
412
+ this.addEffect(effect)
413
+ }
414
+ }
415
+
416
+ processAccountChanges({action, before, after}) {
417
+ switch (action) {
418
+ case 'created':
419
+ const accountCreated = {
420
+ type: effectTypes.accountCreated,
421
+ account: after.address
422
+ }
423
+ if (after.sponsor) {
424
+ accountCreated.sponsor = after.sponsor
425
+ }
426
+ this.addEffect(accountCreated)
427
+ if (after.balance > 0) {
428
+ this.credit(after.balance, 'XLM', after.address, after.balance)
429
+ }
430
+ break
431
+ case 'updated':
432
+ if (before.balance !== after.balance) {
433
+ this.processBalanceChange(after.address, 'XLM', before.balance, after.balance)
434
+ }
435
+ //other operations do not yield signer sponsorship effects
436
+ if (this.operation.type === 'setOptions' || this.operation.type === 'revokeSignerSponsorship') {
437
+ this.processSignerSponsorshipEffects({before, after})
438
+ }
439
+ break
440
+ case 'removed':
441
+ if (before.balance > 0) {
442
+ this.debit(before.balance, 'XLM', before.address, '0')
443
+ }
444
+ const accountRemoved = {
445
+ type: effectTypes.accountRemoved
446
+ }
447
+ if (before.sponsor) {
448
+ accountRemoved.sponsor = before.sponsor
449
+ }
450
+ this.addEffect(accountRemoved)
451
+ break
452
+ }
453
+
454
+ for (const effect of analyzeSignerChanges(before, after)) {
455
+ this.addEffect(effect)
456
+ }
457
+ }
458
+
459
+ processTrustlineEffectsChanges({action, before, after}) {
460
+ const snapshot = (after || before)
461
+ const trustEffect = {
462
+ type: '',
463
+ source: snapshot.account,
464
+ asset: snapshot.asset,
465
+ kind: snapshot.asset.includes('-') ? 'asset' : 'poolShares',
466
+ flags: snapshot.flags
467
+ }
468
+ if (snapshot.sponsor) {
469
+ trustEffect.sponsor = snapshot.sponsor
470
+ }
471
+ switch (action) {
472
+ case 'created':
473
+ trustEffect.type = effectTypes.trustlineCreated
474
+ trustEffect.limit = snapshot.limit
475
+ break
476
+ case 'updated':
477
+ if (before.balance !== after.balance) {
478
+ this.processBalanceChange(after.account, after.asset, before.balance, after.balance)
479
+ }
480
+ if (before.limit === after.limit && before.flags === after.flags)
481
+ return
482
+ trustEffect.type = effectTypes.trustlineUpdated
483
+ trustEffect.limit = snapshot.limit
484
+ break
485
+ case 'removed':
486
+ trustEffect.type = effectTypes.trustlineRemoved
487
+ if (before.balance > 0) {
488
+ this.processBalanceChange(before.account, before.asset, before.balance, '0')
489
+ }
490
+ break
491
+ }
492
+ this.addEffect(trustEffect)
493
+ }
494
+
495
+ processBalanceChange(account, asset, beforeBalance, afterBalance) {
496
+ const balanceChange = BigInt(afterBalance) - BigInt(beforeBalance)
497
+ if (balanceChange < 0n) {
498
+ this.debit((-balanceChange).toString(), asset, account, afterBalance)
499
+ } else {
500
+ this.credit(balanceChange.toString(), asset, account, afterBalance)
501
+ }
502
+ }
503
+
504
+ processSignerSponsorshipEffects({before, after}) {
505
+ if (!before.signerSponsoringIDs?.length && !after.signerSponsoringIDs?.length)
506
+ return
507
+ const [beforeMap, afterMap] = [before, after].map(state => {
508
+ const signersMap = {}
509
+ if (state.signerSponsoringIDs?.length) {
510
+ for (let i = 0; i < state.signers.length; i++) {
511
+ const sponsor = state.signerSponsoringIDs[i]
512
+ if (sponsor) { //add only sponsored signers to the map
513
+ signersMap[state.signers[i].key] = sponsor
514
+ }
515
+ }
516
+ }
517
+ return signersMap
518
+ })
519
+
520
+ for (const signerKey of Object.keys(beforeMap)) {
521
+ const newSponsor = afterMap[signerKey]
522
+ if (!newSponsor) {
523
+ this.addEffect({
524
+ type: effectTypes.signerSponsorshipRemoved,
525
+ account: before.address,
526
+ signer: signerKey,
527
+ prevSponsor: beforeMap[signerKey]
528
+ })
529
+ break
530
+ }
531
+ if (newSponsor !== beforeMap[signerKey]) {
532
+ this.addEffect({
533
+ type: effectTypes.signerSponsorshipUpdated,
534
+ account: before.address,
535
+ signer: signerKey,
536
+ sponsor: newSponsor,
537
+ prevSponsor: beforeMap[signerKey]
538
+ })
539
+ break
540
+ }
541
+ }
542
+
543
+ for (const signerKey of Object.keys(afterMap)) {
544
+ const prevSponsor = beforeMap[signerKey]
545
+ if (!prevSponsor) {
546
+ this.addEffect({
547
+ type: effectTypes.signerSponsorshipCreated,
548
+ account: after.address,
549
+ signer: signerKey,
550
+ sponsor: afterMap[signerKey]
551
+ })
552
+ break
553
+ }
554
+ }
555
+ }
556
+
557
+ processOfferChanges({action, before, after}) {
558
+ const snapshot = after || before
559
+ const effect = {
560
+ type: effectTypes.offerRemoved,
561
+ owner: snapshot.account,
562
+ offer: snapshot.id,
563
+ asset: snapshot.asset,
564
+ flags: snapshot.flags
565
+ }
566
+ if (snapshot.sponsor) {
567
+ effect.sponsor = snapshot.sponsor
568
+ }
569
+ switch (action) {
570
+ case 'created':
571
+ effect.type = effectTypes.offerCreated
572
+ effect.amount = after.amount
573
+ effect.price = after.price
574
+ break
575
+ case 'updated':
576
+ if (before.price === after.price && before.asset.join() === after.asset.join() && before.amount === after.amount)
577
+ return //no changes - skip
578
+ effect.type = effectTypes.offerUpdated
579
+ effect.amount = after.amount
580
+ effect.price = after.price
581
+ break
582
+ }
583
+ this.addEffect(effect)
584
+ }
585
+
586
+ processLiquidityPoolChanges({action, before, after}) {
587
+ const snapshot = after || before
588
+ const effect = {
589
+ type: effectTypes.liquidityPoolRemoved,
590
+ pool: snapshot.pool
591
+ }
592
+ if (snapshot.sponsor) {
593
+ effect.sponsor = snapshot.sponsor
594
+ }
595
+ switch (action) {
596
+ case 'created':
597
+ Object.assign(effect, {
598
+ type: effectTypes.liquidityPoolCreated,
599
+ reserves: after.asset.map(asset => ({asset, amount: '0'})),
600
+ shares: '0',
601
+ accounts: 1
602
+ })
603
+ this.addEffect(effect, this.effects.findIndex(e => e.pool === effect.pool || e.asset === effect.pool))
604
+ return
605
+ case 'updated':
606
+ Object.assign(effect, {
607
+ type: effectTypes.liquidityPoolUpdated,
608
+ reserves: after.asset.map((asset, i) => ({
609
+ asset,
610
+ amount: after.amount[i]
611
+ })),
612
+ shares: after.shares,
613
+ accounts: after.accounts
614
+ })
615
+ break
616
+ }
617
+ this.addEffect(effect)
618
+ }
619
+
620
+ processClaimableBalanceChanges({action, before, after}) {
621
+ switch (action) {
622
+ case 'created':
623
+ this.addEffect({
624
+ type: effectTypes.claimableBalanceCreated,
625
+ sponsor: after.sponsor,
626
+ balance: after.balanceId,
627
+ asset: after.asset,
628
+ amount: after.amount,
629
+ claimants: after.claimants
630
+ })
631
+ break
632
+ case 'removed':
633
+ this.addEffect({
634
+ type: effectTypes.claimableBalanceRemoved,
635
+ sponsor: before.sponsor,
636
+ balance: before.balanceId,
637
+ asset: before.asset,
638
+ amount: before.amount,
639
+ claimants: before.claimants
640
+ })
641
+ break
642
+ case 'updated':
643
+ //nothing to process here
644
+ break
645
+ }
646
+ }
647
+
648
+ processDataEntryChanges({action, before, after}) {
649
+ const effect = {type: ''}
650
+ const {sponsor, name, value} = after || before
651
+ effect.name = name
652
+ effect.value = value && value.toString('base64')
653
+ switch (action) {
654
+ case 'created':
655
+ effect.type = effectTypes.dataEntryCreated
656
+ break
657
+ case 'updated':
658
+ if (before.value === after.value)
659
+ return //value has not changed
660
+ effect.type = effectTypes.dataEntryUpdated
661
+ break
662
+ case 'removed':
663
+ effect.type = effectTypes.dataEntryRemoved
664
+ delete effect.value
665
+ break
666
+ }
667
+ if (sponsor) {
668
+ effect.sponsor = sponsor
669
+ }
670
+ this.addEffect(effect)
671
+ }
672
+
673
+ processContractDataChanges({action, before, after}) {
674
+ const {owner, key, value, durability} = after || before
675
+ const effect = {
676
+ type: '',
677
+ owner,
678
+ key,
679
+ value,
680
+ durability
681
+ }
682
+ switch (action) {
683
+ case 'created':
684
+ effect.type = effectTypes.contractDataCreated
685
+ break
686
+ case 'updated':
687
+ if (before.value === after.value)
688
+ return //value has not changed
689
+ effect.type = effectTypes.contractDataUpdated
690
+ break
691
+ case 'removed':
692
+ effect.type = effectTypes.contractDataRemoved
693
+ delete effect.value
694
+ break
695
+ }
696
+ this.addEffect(effect)
697
+ this.processContractBalance(effect)
698
+ }
699
+
700
+ processContractBalance(effect) {
701
+ const parsedKey = xdr.ScVal.fromXDR(effect.key, 'base64')
702
+ if (parsedKey._arm !== 'vec')
703
+ return
704
+ const keyParts = parsedKey._value
705
+ if (!(keyParts instanceof Array) || keyParts.length !== 2)
706
+ return
707
+ if (keyParts[0]._arm !== 'sym' || keyParts[1]._arm !== 'address' || keyParts[0]._value.toString() !== 'Balance')
708
+ return
709
+ const account = xdrParseScVal(keyParts[1])
710
+ const balanceEffects = this.effects.filter(e => (e.type === effectTypes.accountCredited || e.type === effectTypes.accountDebited) && e.source === account && e.asset === effect.owner)
711
+ 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
712
+ return
713
+ if (effect.type === effectTypes.contractDataRemoved) { //balance completely removed
714
+ balanceEffects[0].balance = '0'
715
+ return
716
+ }
717
+ const value = xdr.ScVal.fromXDR(effect.value, 'base64')
718
+ if (value._arm !== 'map')
719
+ return
720
+ const parsedValue = xdrParseScVal(value)
721
+ if (typeof parsedValue.clawback !== 'boolean' || typeof parsedValue.authorized !== 'boolean' || typeof parsedValue.amount !== 'string')
722
+ return
723
+ //set transfer effect balance
724
+ balanceEffects[0].balance = parsedValue.amount
725
+ }
726
+
727
+ processContractChanges({action, after}) {
728
+ if (action === 'updated')
729
+ return // TODO: check whether any contract properties changed
730
+ if (action !== 'created')
731
+ throw new UnexpectedTxMetaChangeError({type: 'contract', action})
732
+ const {kind, contract, hash} = after
733
+ if (this.effects.some(e => e.contract === contract))
734
+ return //skip contract creation effects processed by top-level createContract operation call
735
+ const effect = {
736
+ type: effectTypes.contractCreated,
737
+ contract,
738
+ kind,
739
+ wasmHash: hash
740
+ }
741
+ this.addEffect(effect)
742
+
743
+ }
744
+
745
+ processChanges() {
746
+ for (const change of this.changes)
747
+ switch (change.type) {
748
+ case 'account':
749
+ this.processAccountChanges(change)
750
+ break
751
+ case 'trustline':
752
+ this.processTrustlineEffectsChanges(change)
753
+ break
754
+ case 'claimableBalance':
755
+ this.processClaimableBalanceChanges(change)
756
+ break
757
+ case 'offer':
758
+ this.processOfferChanges(change)
759
+ break
760
+ case 'liquidityPool':
761
+ this.processLiquidityPoolChanges(change)
762
+ break
763
+ case 'data':
764
+ this.processDataEntryChanges(change)
765
+ break
766
+ case 'contractData':
767
+ //this.processContractDataChanges(change)
768
+ break
769
+ case 'contract':
770
+ this.processContractChanges(change)
771
+ break
772
+ default:
773
+ throw new UnexpectedTxMetaChangeError(change)
774
+ }
775
+ }
776
+
777
+ processAssetSupplyEffects() {
778
+ const supplyProcessor = new AssetSupplyProcessor(this.effects)
779
+ for (const effect of supplyProcessor.resolve()) {
780
+ this.addEffect(effect, effect.type === effectTypes.assetMinted ?
781
+ this.effects.findIndex(e => e.asset === effect.asset || e.assets?.find(a => a.asset === effect.asset)) :
782
+ undefined)
783
+ }
784
+ }
785
+ }
786
+
787
+ /**
788
+ * Generates fee charged effect
789
+ * @param {{}} tx - Transaction
790
+ * @param {String} source - Source account
791
+ * @param {String} chargedAmount - Charged amount
792
+ * @param {Boolean} [feeBump] - Is fee bump transaction
793
+ * @returns {{}} - Fee charged effect
794
+ */
795
+ function processFeeChargedEffect(tx, source, chargedAmount, feeBump = false) {
796
+ if (tx._switch) { //raw XDR
797
+ const txXdr = tx.value().tx()
798
+ tx = {
799
+ source: xdrParseAccountAddress((txXdr.feeSource ? txXdr.feeSource : txXdr.sourceAccount).call(txXdr)),
800
+ fee: txXdr.fee().toString()
801
+ }
802
+ }
803
+ const res = {
804
+ type: effectTypes.feeCharged,
805
+ source,
806
+ asset: 'XLM',
807
+ bid: tx.fee,
808
+ charged: chargedAmount
809
+ }
810
+ if (feeBump) {
811
+ res.bump = true
812
+ }
813
+ return res
814
+ }
815
+
816
+ function normalizeAddress(address) {
817
+ const prefix = address[0]
818
+ if (prefix === 'G')
819
+ return address
820
+ if (prefix !== 'M')
821
+ throw new TypeError('Expected ED25519 or Muxed address')
822
+ const rawBytes = StrKey.decodeMed25519PublicKey(address)
823
+ return StrKey.encodeEd25519PublicKey(rawBytes.subarray(0, 32))
824
+ }
825
+
826
+ module.exports = {EffectsAnalyzer, processFeeChargedEffect}