@stellar-expert/tx-meta-effects-parser 5.0.4 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stellar-expert/tx-meta-effects-parser",
3
- "version": "5.0.4",
3
+ "version": "5.1.0",
4
4
  "description": "Low-level effects parser for Stellar transaction results and meta XDR",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -8,7 +8,7 @@ class AssetSupplyProcessor {
8
8
  * @param {EffectsAnalyzer} effectsAnalyzer
9
9
  */
10
10
  constructor(effectsAnalyzer) {
11
- this.assetTransfers = {}
11
+ this.assetTransfers = new Map()
12
12
  this.processXlmBalances = effectsAnalyzer.isContractCall
13
13
  this.effectsAnalyzer = effectsAnalyzer
14
14
  }
@@ -19,7 +19,7 @@ class AssetSupplyProcessor {
19
19
  */
20
20
  effectsAnalyzer
21
21
  /**
22
- * @type {Object.<String,BigInt>}
22
+ * @type {Map<String,BigInt>}
23
23
  * @private
24
24
  */
25
25
  assetTransfers
@@ -28,6 +28,32 @@ class AssetSupplyProcessor {
28
28
  * @private
29
29
  */
30
30
  processXlmBalances = false
31
+ /**
32
+ * @type {Number}
33
+ * @private
34
+ */
35
+ supplyChanges = 0
36
+
37
+ /**
38
+ * Calculate differences and generate minted/burned effects if needed
39
+ */
40
+ analyze() {
41
+ for (const effect of this.effectsAnalyzer.effects) {
42
+ this.processEffect(effect)
43
+ }
44
+ for (const [asset, amount] of this.assetTransfers.entries()) {
45
+ if (amount > 0n) {
46
+ this.effectsAnalyzer.mint(asset, amount.toString(), true)
47
+ this.supplyChanges |= 2
48
+ } else if (amount < 0n) {
49
+ this.effectsAnalyzer.burn(asset, (-amount).toString())
50
+ this.supplyChanges |= 1
51
+ }
52
+ }
53
+ if ((this.supplyChanges & 3) === 3) { //analyze possible collapsible mints only if both mint and burn effects recorded
54
+ new CollapsibleMintsAnalyzer(this.effectsAnalyzer).removeCollapsingMints()
55
+ }
56
+ }
31
57
 
32
58
  /**
33
59
  * Process generated operation effect
@@ -37,15 +63,23 @@ class AssetSupplyProcessor {
37
63
  switch (effect.type) {
38
64
  case effectTypes.accountCredited:
39
65
  case effectTypes.claimableBalanceCreated:
66
+ //increase supply
67
+ this.increase(effect.asset, effect.amount)
68
+ break
40
69
  case effectTypes.assetBurned:
41
70
  //increase supply
42
71
  this.increase(effect.asset, effect.amount)
72
+ this.supplyChanges |= 1
43
73
  break
44
74
  case effectTypes.accountDebited:
45
75
  case effectTypes.claimableBalanceRemoved:
76
+ //decrease supply
77
+ this.decrease(effect.asset, effect.amount)
78
+ break
46
79
  case effectTypes.assetMinted:
47
80
  //decrease supply
48
81
  this.decrease(effect.asset, effect.amount)
82
+ this.supplyChanges |= 2
49
83
  break
50
84
  case effectTypes.liquidityPoolDeposited:
51
85
  //increase supply for every deposited asset (if liquidity provider is an issuer)
@@ -73,22 +107,6 @@ class AssetSupplyProcessor {
73
107
  }
74
108
  }
75
109
 
76
- /**
77
- * Calculate differences and generate minted/burned effects if needed
78
- */
79
- analyze() {
80
- for (const effect of this.effectsAnalyzer.effects) {
81
- this.processEffect(effect)
82
- }
83
- for (const [asset, amount] of Object.entries(this.assetTransfers)) {
84
- if (amount > 0n) {
85
- this.effectsAnalyzer.mint(asset, amount.toString(), true)
86
- } else if (amount < 0n) {
87
- this.effectsAnalyzer.burn(asset, (-amount).toString())
88
- }
89
- }
90
- }
91
-
92
110
  /**
93
111
  * @param {String} asset
94
112
  * @param {String} amount
@@ -97,7 +115,7 @@ class AssetSupplyProcessor {
97
115
  increase(asset, amount) {
98
116
  if (!this.shouldProcessAsset(asset))
99
117
  return
100
- this.assetTransfers[asset] = (this.assetTransfers[asset] || 0n) + BigInt(amount)
118
+ this.assetTransfers.set(asset, (this.assetTransfers.get(asset) || 0n) + BigInt(amount))
101
119
  }
102
120
 
103
121
  /**
@@ -108,7 +126,7 @@ class AssetSupplyProcessor {
108
126
  decrease(asset, amount) {
109
127
  if (!this.shouldProcessAsset(asset))
110
128
  return
111
- this.assetTransfers[asset] = (this.assetTransfers[asset] || 0n) - BigInt(amount)
129
+ this.assetTransfers.set(asset, (this.assetTransfers.get(asset) || 0n) - BigInt(amount))
112
130
  }
113
131
 
114
132
  /**
@@ -122,4 +140,65 @@ class AssetSupplyProcessor {
122
140
  }
123
141
  }
124
142
 
143
+ class CollapsibleMintsAnalyzer {
144
+ /**
145
+ * @param {EffectsAnalyzer} effectsAnalyzer
146
+ */
147
+ constructor(effectsAnalyzer) {
148
+ this.effectsAnalyzer = effectsAnalyzer
149
+ this.supply = new Map()
150
+ }
151
+
152
+ /**
153
+ * @type {EffectsAnalyzer}
154
+ * @private
155
+ */
156
+ effectsAnalyzer
157
+ /**
158
+ * @type {Map<String,[]>}
159
+ * @private
160
+ */
161
+ supply
162
+
163
+ removeCollapsingMints() {
164
+ const {effects} = this.effectsAnalyzer
165
+ for (const effect of effects) {
166
+ if (effect.type === effectTypes.assetMinted || effect.type === effectTypes.assetBurned) {
167
+ this.addEffectToSupplyCounter(effect)
168
+ }
169
+ }
170
+ for (const [asset, assetEffects] of this.supply.entries()) {
171
+ if (assetEffects.length < 2)
172
+ continue //skip non-collapsible effects
173
+ //aggregate amount
174
+ let sum = 0n
175
+ let position
176
+ for (const effect of assetEffects) {
177
+ sum += effect.type === effectTypes.assetMinted ? BigInt(effect.amount) : -BigInt(effect.amount) //add to the running total
178
+ position = effects.indexOf(effect) //find effect position in the parent effects container
179
+ effects.splice(position, 1) //remove the effect from the parent container
180
+ }
181
+ if (sum > 0n) { //asset minted
182
+ this.effectsAnalyzer.mint(asset, sum.toString(), true) //insert mint effect
183
+ } else if (sum < 0n) { //asset burned
184
+ this.effectsAnalyzer.burn(asset, sum.toString(), position) //insert burn effect at the position of the last removed effect
185
+ }
186
+ //if sum=0 then both effects were annihilated and removed
187
+ }
188
+ }
189
+
190
+ /**
191
+ * @param {{}} effect
192
+ * @private
193
+ */
194
+ addEffectToSupplyCounter(effect) {
195
+ let container = this.supply.get(effect.asset)
196
+ if (!container) {
197
+ container = []
198
+ this.supply.set(effect.asset, container)
199
+ }
200
+ container.push(effect)
201
+ }
202
+ }
203
+
125
204
  module.exports = AssetSupplyProcessor
@@ -161,7 +161,7 @@ class EventsAnalyzer {
161
161
  case 'mint': {
162
162
  if (!matchEventTopicsShape(topics, ['address', 'address', 'str?']))
163
163
  return //throw new Error('Non-standard event')
164
- const to = xdrParseScVal(topics[1])
164
+ const to = xdrParseScVal(topics[2])
165
165
  const amount = processEventBodyValue(body.data())
166
166
  if (!this.matchInvocationEffect(e => e.function === 'mint' && matchArrays([to, amount], e.args)))
167
167
  return
@@ -191,7 +191,6 @@ class EventsAnalyzer {
191
191
  case 'clawback': {
192
192
  if (!matchEventTopicsShape(topics, ['address', 'address', 'str?']))
193
193
  return //throw new Error('Non-standard event')
194
- const admin = xdrParseScVal(topics[1])
195
194
  const from = xdrParseScVal(topics[2])
196
195
  const amount = processEventBodyValue(body.data())
197
196
  if (!this.matchInvocationEffect(e => e.function === 'clawback' && matchArrays([from, amount], e.args)))
@@ -298,7 +298,14 @@ function parseContractData(value) {
298
298
  break
299
299
  case 'contractExecutableWasm':
300
300
  entry.kind = 'wasm'
301
- entry.hash = valueAttr.instance().executable().wasmHash().toString('hex')
301
+ const instance = valueAttr.instance()._attributes
302
+ entry.hash = instance.executable.wasmHash().toString('hex')
303
+ if (instance.storage?.length) {
304
+ entry.storage = instance.storage.map(entry => ({
305
+ key: entry.key().toXDR('base64'),
306
+ val: entry.val().toXDR('base64')
307
+ }))
308
+ }
302
309
  break
303
310
  default:
304
311
  throw new TxMetaEffectParserError('Unsupported executable type: ' + type)
@@ -145,12 +145,12 @@ class EffectsAnalyzer {
145
145
  }, position)
146
146
  }
147
147
 
148
- burn(asset, amount) {
148
+ burn(asset, amount, position = undefined) {
149
149
  this.addEffect({
150
150
  type: effectTypes.assetBurned,
151
151
  asset,
152
152
  amount
153
- })
153
+ }, position)
154
154
  }
155
155
 
156
156
  setOptions() {
@@ -694,33 +694,6 @@ class EffectsAnalyzer {
694
694
  this.addEffect(effect)
695
695
  }
696
696
 
697
- processContractDataChanges({action, before, after}) {
698
- const {owner, key, value, durability} = after || before
699
- const effect = {
700
- type: '',
701
- owner,
702
- key,
703
- value,
704
- durability
705
- }
706
- switch (action) {
707
- case 'created':
708
- effect.type = effectTypes.contractDataCreated
709
- break
710
- case 'updated':
711
- if (before.value === after.value)
712
- return //value has not changed
713
- effect.type = effectTypes.contractDataUpdated
714
- break
715
- case 'removed':
716
- effect.type = effectTypes.contractDataRemoved
717
- delete effect.value
718
- break
719
- }
720
- this.addEffect(effect)
721
- this.processContractBalance(effect)
722
- }
723
-
724
697
  processContractBalance(effect) {
725
698
  const parsedKey = xdr.ScVal.fromXDR(effect.key, 'base64')
726
699
  if (parsedKey._arm !== 'vec')
@@ -748,22 +721,108 @@ class EffectsAnalyzer {
748
721
  balanceEffects[0].balance = parsedValue.amount
749
722
  }
750
723
 
751
- processContractChanges({action, after}) {
752
- if (action === 'updated')
753
- return // TODO: check whether any contract properties changed
754
- if (action !== 'created')
724
+ processContractChanges({action, before, after}) {
725
+ if (action !== 'created' && action !== 'updated')
755
726
  throw new UnexpectedTxMetaChangeError({type: 'contract', action})
756
727
  const {kind, contract, hash} = after
757
- if (this.effects.some(e => e.contract === contract))
758
- return //skip contract creation effects processed by top-level createContract operation call
759
728
  const effect = {
760
729
  type: effectTypes.contractCreated,
761
730
  contract,
762
731
  kind,
763
732
  wasmHash: hash
764
733
  }
734
+ if (action === 'created') {
735
+ if (this.effects.some(e => e.contract === contract))
736
+ return //skip contract creation effects processed by top-level createContract operation call
737
+ } else if (action === 'updated') {
738
+ effect.type = effectTypes.contractUpdated
739
+ effect.prevWasmHash = before.hash
740
+ if (before.storage?.length || after.storage?.length) {
741
+ this.processInstanceDataChanges(before, after)
742
+ }
743
+ if (before.hash === hash) //skip if hash unchanged
744
+ return
745
+ }
746
+ this.addEffect(effect)
747
+ }
748
+
749
+ processContractDataChanges({action, before, after}) {
750
+ const {owner, key, durability} = after || before
751
+ const effect = {
752
+ type: '',
753
+ owner,
754
+ durability,
755
+ key
756
+ }
757
+ switch (action) {
758
+ case 'created':
759
+ effect.type = effectTypes.contractDataCreated
760
+ effect.value = after.value
761
+ break
762
+ case 'updated':
763
+ if (before.value === after.value)
764
+ return //value has not changed
765
+ effect.type = effectTypes.contractDataUpdated
766
+ effect.value = after.value
767
+ effect.prevValue = before.value
768
+ break
769
+ case 'removed':
770
+ effect.type = effectTypes.contractDataRemoved
771
+ effect.prevValue = before.value
772
+ break
773
+ }
765
774
  this.addEffect(effect)
775
+ this.processContractBalance(effect)
776
+ }
766
777
 
778
+ processInstanceDataChanges(before, after) {
779
+ const storageBefore = before.storage
780
+ const storageAfter = [...after.storage || []]
781
+ for (const {key, val} of storageBefore) {
782
+ let newVal
783
+ for (let i = 0; i < storageAfter.length; i++) {
784
+ const afterValue = storageAfter[i]
785
+ if (afterValue.key === key) {
786
+ newVal = afterValue.val //update new value
787
+ storageAfter.splice(i, 1) //remove from array to simplify iteration
788
+ break
789
+ }
790
+ }
791
+ if (newVal === undefined) { //removed
792
+ const effect = {
793
+ type: effectTypes.contractDataRemoved,
794
+ owner: after.contract || before.contract,
795
+ key,
796
+ prevValue: val,
797
+ durability: 'instance'
798
+ }
799
+ this.addEffect(effect)
800
+ continue
801
+ }
802
+ if (val === newVal) //value has not changed
803
+ continue
804
+
805
+ const effect = {
806
+ type: effectTypes.contractDataUpdated,
807
+ owner: after.contract || before.contract,
808
+ key,
809
+ value: newVal,
810
+ prevValue: val,
811
+ durability: 'instance'
812
+ }
813
+ this.addEffect(effect)
814
+ }
815
+ //iterate all storage items left
816
+ for (const {key, val} of storageAfter) {
817
+ const effect = {
818
+ type: effectTypes.contractDataCreated,
819
+ owner: after.contract || before.contract,
820
+ key,
821
+ value: val,
822
+ durability: 'instance'
823
+ }
824
+ this.addEffect(effect)
825
+ }
767
826
  }
768
827
 
769
828
  processChanges() {