@stellar-expert/tx-meta-effects-parser 5.0.0-beta13 → 5.0.0-beta15

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.0-beta13",
3
+ "version": "5.0.0-beta15",
4
4
  "description": "Low-level effects parser for Stellar transaction results and meta XDR",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -4,20 +4,25 @@ const effectTypes = require('./effect-types')
4
4
  * Effect supply computation processor
5
5
  */
6
6
  class AssetSupplyProcessor {
7
- constructor(effects) {
7
+ /**
8
+ * @param {EffectsAnalyzer} effectsAnalyzer
9
+ */
10
+ constructor(effectsAnalyzer) {
8
11
  this.assetTransfers = {}
9
- this.processXlmBalances = effects.some(e => e.type === 'contractInvoked')
10
- for (const effect of effects) {
11
- this.processEffect(effect)
12
- }
12
+ this.processXlmBalances = effectsAnalyzer.isContractCall
13
+ this.effectsAnalyzer = effectsAnalyzer
13
14
  }
14
15
 
16
+ /**
17
+ * @type {EffectsAnalyzer}
18
+ * @private
19
+ */
20
+ effectsAnalyzer
15
21
  /**
16
22
  * @type {Object.<String,BigInt>}
17
23
  * @private
18
24
  */
19
25
  assetTransfers
20
-
21
26
  /**
22
27
  * @type {Boolean}
23
28
  * @private
@@ -70,28 +75,18 @@ class AssetSupplyProcessor {
70
75
 
71
76
  /**
72
77
  * Calculate differences and generate minted/burned effects if needed
73
- * @return {{}[]}
74
78
  */
75
- resolve() {
76
- const res = []
79
+ analyze() {
80
+ for (const effect of this.effectsAnalyzer.effects) {
81
+ this.processEffect(effect)
82
+ }
77
83
  for (const [asset, amount] of Object.entries(this.assetTransfers)) {
78
- if (amount === 0n)
79
- continue
80
- const effect = {
81
- type: effectTypes.assetMinted,
82
- asset
83
- }
84
84
  if (amount > 0n) {
85
- effect.type = effectTypes.assetMinted
86
- effect.amount = amount.toString()
87
- }
88
- if (amount < 0n) {
89
- effect.type = effectTypes.assetBurned
90
- effect.amount = (-amount).toString()
85
+ this.effectsAnalyzer.mint(asset, amount.toString(), true)
86
+ } else if (amount < 0n) {
87
+ this.effectsAnalyzer.burn(asset, (-amount).toString())
91
88
  }
92
- res.push(effect)
93
89
  }
94
- return res
95
90
  }
96
91
 
97
92
  /**
@@ -1,5 +1,6 @@
1
1
  const {StrKey} = require('@stellar/stellar-base')
2
- const {xdrParseScVal} = require('./tx-xdr-parser-utils')
2
+ const {xdrParseScVal, xdrParseAsset, isContractAddress, toStellarAsset} = require('./tx-xdr-parser-utils')
3
+ const {contractIdFromAsset} = require('./contract-preimage-encoder')
3
4
  const effectTypes = require('./effect-types')
4
5
 
5
6
  const EVENT_TYPES = {
@@ -115,20 +116,46 @@ class EventsAnalyzer {
115
116
  return
116
117
  const from = xdrParseScVal(topics[1])
117
118
  const to = xdrParseScVal(topics[2])
118
- const asset = contractId //topics[3]? xdrParseScVal(topics[3]) || contractId
119
+ if (to === from) //self transfer - nothing happens
120
+ return // TODO: need additional checks
121
+ const sorobanAsset = contractId
119
122
  const amount = processEventBodyValue(body.data())
120
123
  if (!this.matchInvocationEffect(e =>
121
124
  (e.function === 'transfer' && matchArrays([from, to, amount], e.args)) ||
122
125
  (e.function === 'transfer_from' && matchArrays([undefined, from, to, amount], e.args))
123
126
  ))
124
127
  return
125
- const isSorobanAsset = isContractAddress(asset)
126
- if (!isSorobanAsset || isContractAddress(from)) {
127
- this.debit(from, asset, amount)
128
+ let classicAsset
129
+ if (topics.length > 3) {
130
+ classicAsset = xdrParseAsset(xdrParseScVal(topics[3]))
131
+ //validate
132
+ if (contractIdFromAsset(toStellarAsset(classicAsset), this.effectAnalyzer.network) !== sorobanAsset)
133
+ return //not an SAC transfer
128
134
  }
129
- if (!isSorobanAsset || isContractAddress(to)) {
130
- this.credit(to, asset, amount)
135
+ if (classicAsset && (classicAsset.includes(from) || classicAsset.includes(to))) { //SAC transfer by asset issuer
136
+ if (classicAsset.includes(from)) {
137
+ this.effectAnalyzer.mint(sorobanAsset, amount)
138
+ this.effectAnalyzer.credit(amount, isContractAddress(to) ? sorobanAsset : classicAsset, to)
139
+ }
140
+ if (classicAsset.includes(to)) {
141
+ this.effectAnalyzer.debit(amount, isContractAddress(from) ? sorobanAsset : classicAsset, from)
142
+ this.effectAnalyzer.burn(sorobanAsset, amount)
143
+ }
144
+ } else { //other cases
145
+ if (classicAsset && !isContractAddress(from)) { //classic asset bridged to Soroban
146
+ this.effectAnalyzer.burn(classicAsset, amount)
147
+ this.effectAnalyzer.mint(sorobanAsset, amount)
148
+ } else {
149
+ this.effectAnalyzer.debit(amount, sorobanAsset, from)
150
+ }
151
+ if (classicAsset && !isContractAddress(to)) { //classic asset bridged from Soroban
152
+ this.effectAnalyzer.burn(sorobanAsset, amount)
153
+ this.effectAnalyzer.mint(classicAsset, amount)
154
+ } else {
155
+ this.effectAnalyzer.credit(amount, sorobanAsset, to)
156
+ }
131
157
  }
158
+
132
159
  }
133
160
  break
134
161
  case 'mint': {
@@ -143,7 +170,7 @@ class EventsAnalyzer {
143
170
  asset: contractId,
144
171
  amount
145
172
  })
146
- this.credit(to, contractId, amount)
173
+ this.effectAnalyzer.credit(amount, contractId, to)
147
174
  }
148
175
  break
149
176
  case 'burn': {
@@ -156,12 +183,9 @@ class EventsAnalyzer {
156
183
  (e.function === 'burn_from' && matchArrays([undefined, from, amount], e.args))
157
184
  ))
158
185
  return
159
- this.debit(from, contractId, amount)
160
- this.effectAnalyzer.addEffect({
161
- type: effectTypes.assetBurned,
162
- asset: contractId,
163
- amount
164
- })
186
+
187
+ this.effectAnalyzer.debit(amount, contractId, from)
188
+ this.effectAnalyzer.burn(contractId, amount)
165
189
  }
166
190
  break
167
191
  case 'clawback': {
@@ -172,12 +196,8 @@ class EventsAnalyzer {
172
196
  const amount = processEventBodyValue(body.data())
173
197
  if (!this.matchInvocationEffect(e => e.function === 'clawback' && matchArrays([from, amount], e.args)))
174
198
  return
175
- this.debit(from, contractId, amount)
176
- this.effectAnalyzer.addEffect({
177
- type: effectTypes.assetBurned,
178
- asset: contractId,
179
- amount
180
- })
199
+ this.effectAnalyzer.debit(amount, contractId, from)
200
+ this.effectAnalyzer.burn(contractId, amount)
181
201
  }
182
202
  break
183
203
  //TODO: process token allowance, authorization approval, and admin modification for SAC contracts
@@ -214,48 +234,6 @@ class EventsAnalyzer {
214
234
  return null
215
235
  }
216
236
 
217
- /**
218
- * @param {String} from
219
- * @param {String} asset
220
- * @param {String} amount
221
- * @private
222
- */
223
- debit(from, asset, amount) {
224
- this.effectAnalyzer.debit(amount, asset, from)
225
-
226
- //debit from account
227
- //TODO: check debits of Soroban assets from account
228
- //if (token.anchoredAsset)
229
- //return //skip processing changes for classic assets - they are processed elsewhere
230
- /*this.effectAnalyzer.addEffect({
231
- type: effectTypes.accountDebited,
232
- source: from,
233
- asset: token.asset,
234
- amount
235
- })*/
236
- }
237
-
238
- /**
239
- * @param {String} to
240
- * @param {String} asset
241
- * @param {String} amount
242
- * @private
243
- */
244
- credit(to, asset, amount) {
245
- this.effectAnalyzer.credit(amount, asset, to)
246
-
247
- //credit account
248
- //TODO: check credits of Soroban assets
249
- //if (token.anchoredAsset)
250
- //return //skip processing changes for classic assets - they are processed elsewhere
251
- /*this.effectAnalyzer.addEffect({
252
- type: effectTypes.accountCredited,
253
- source: to,
254
- asset: token.asset,
255
- amount
256
- })*/
257
- }
258
-
259
237
  matchInvocationEffect(cb) {
260
238
  return this.effectAnalyzer.effects.find(e => e.type === effectTypes.contractInvoked && cb(e))
261
239
  }
@@ -303,8 +281,4 @@ function processEventBodyValue(value) {
303
281
  return xdrParseScVal(value) //other scValue
304
282
  }
305
283
 
306
- function isContractAddress(address) {
307
- return address.length === 56 && address[0] === 'C'
308
- }
309
-
310
284
  module.exports = EventsAnalyzer
@@ -15,6 +15,7 @@ class EffectsAnalyzer {
15
15
  if (!operation.source)
16
16
  throw new TxMetaEffectParserError('Operation source is not explicitly defined')
17
17
  this.operation = operation
18
+ this.isContractCall = this.operation.type === 'invokeHostFunction'
18
19
  this.result = result
19
20
  this.changes = parseLedgerEntryChanges(meta)
20
21
  this.source = this.operation.source
@@ -27,7 +28,7 @@ class EffectsAnalyzer {
27
28
 
28
29
  /**
29
30
  * @type {{}[]}
30
- * @private
31
+ * @internal
31
32
  * @readonly
32
33
  */
33
34
  effects = []
@@ -60,6 +61,11 @@ class EffectsAnalyzer {
60
61
  * @readonly
61
62
  */
62
63
  source = ''
64
+ /**
65
+ * @type {boolean}
66
+ * @private
67
+ */
68
+ isContractCall = false
63
69
 
64
70
  analyze() {
65
71
  //find appropriate parsing method
@@ -74,13 +80,12 @@ class EffectsAnalyzer {
74
80
  //process Soroban events
75
81
  new EventsAnalyzer(this).analyze()
76
82
  //calculate minted/burned assets
77
- this.processAssetSupplyEffects()
83
+ new AssetSupplyProcessor(this).analyze()
78
84
  //process state data changes in the end
79
85
  for (const change of this.changes)
80
86
  if (change.type === 'contractData') {
81
87
  this.processContractDataChanges(change)
82
88
  }
83
-
84
89
  return this.effects
85
90
  }
86
91
 
@@ -129,6 +134,25 @@ class EffectsAnalyzer {
129
134
  this.addEffect(effect)
130
135
  }
131
136
 
137
+ mint(asset, amount, autoLookupPosition = false) {
138
+ const position = autoLookupPosition ?
139
+ this.effects.findIndex(e => e.asset === asset || e.assets?.find(a => a.asset === asset)) :
140
+ undefined
141
+ this.addEffect({
142
+ type: effectTypes.assetMinted,
143
+ asset,
144
+ amount
145
+ }, position)
146
+ }
147
+
148
+ burn(asset, amount) {
149
+ this.addEffect({
150
+ type: effectTypes.assetBurned,
151
+ asset,
152
+ amount
153
+ })
154
+ }
155
+
132
156
  setOptions() {
133
157
  const sourceAccount = normalizeAddress(this.source)
134
158
  const {before, after} = this.changes.find(ch => ch.type === 'account' && ch.before.address === sourceAccount)
@@ -773,15 +797,6 @@ class EffectsAnalyzer {
773
797
  throw new UnexpectedTxMetaChangeError(change)
774
798
  }
775
799
  }
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
800
  }
786
801
 
787
802
  /**
@@ -1,6 +1,28 @@
1
- const {StrKey, LiquidityPoolId, scValToBigInt, xdr} = require('@stellar/stellar-base')
1
+ const {StrKey, LiquidityPoolId, scValToBigInt, xdr, Asset} = require('@stellar/stellar-base')
2
2
  const {TxMetaEffectParserError} = require('./errors')
3
3
 
4
+ /**
5
+ * @param {String} address
6
+ * @return {Boolean}
7
+ */
8
+ function isContractAddress(address) {
9
+ return address.length === 56 && address[0] === 'C'
10
+ }
11
+
12
+ /**
13
+ * @param {String} assetDescriptor
14
+ * @return {Asset}
15
+ */
16
+ function toStellarAsset(assetDescriptor) {
17
+ if (assetDescriptor === 'XLM')
18
+ return Asset.native()
19
+ if (assetDescriptor.includes('-')) {
20
+ const [code, issuer] = assetDescriptor.split('-')
21
+ return new Asset(code, issuer)
22
+ }
23
+ throw new TypeError('Unsupported asset format ' + assetDescriptor)
24
+ }
25
+
4
26
  /**
5
27
  * Parse account address from XDR representation
6
28
  * @param accountId
@@ -195,12 +217,14 @@ function xdrParseAsset(src) {
195
217
  }
196
218
 
197
219
  if (typeof src === 'string') {
198
- if (src === 'XLM' || src === 'native' || src.includes('-'))
220
+ if (src === 'XLM' || src === 'native')
199
221
  return 'XLM'//already parsed value
200
222
  if (src.includes(':')) {
201
223
  const [code, issuer] = src.split(':')
202
224
  return `${code.replace(/\0+$/, '')}-${issuer}-${code.length > 4 ? 2 : 1}`
203
225
  }
226
+ if (src.includes('-'))
227
+ return src //already parsed
204
228
  if (src.length === 64)
205
229
  return src //pool id
206
230
  }
@@ -272,5 +296,7 @@ module.exports = {
272
296
  xdrParseTradeAtom,
273
297
  xdrParseSignerKey,
274
298
  xdrParsePrice,
275
- xdrParseScVal
299
+ xdrParseScVal,
300
+ isContractAddress,
301
+ toStellarAsset
276
302
  }