@stellar-expert/tx-meta-effects-parser 5.0.0-beta13 → 5.0.0-beta14
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 +1 -1
- package/src/asset-supply-processor.js +18 -23
- package/src/events-analyzer.js +40 -66
- package/src/tx-effects-analyzer.js +27 -12
- package/src/tx-xdr-parser-utils.js +29 -3
package/package.json
CHANGED
|
@@ -4,20 +4,25 @@ const effectTypes = require('./effect-types')
|
|
|
4
4
|
* Effect supply computation processor
|
|
5
5
|
*/
|
|
6
6
|
class AssetSupplyProcessor {
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* @param {EffectsAnalyzer} effectsAnalyzer
|
|
9
|
+
*/
|
|
10
|
+
constructor(effectsAnalyzer) {
|
|
8
11
|
this.assetTransfers = {}
|
|
9
|
-
this.processXlmBalances =
|
|
10
|
-
|
|
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
|
-
|
|
76
|
-
const
|
|
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
|
-
|
|
86
|
-
|
|
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
|
/**
|
package/src/events-analyzer.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
126
|
-
if (
|
|
127
|
-
|
|
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 (
|
|
130
|
-
|
|
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, isContractAddress(from) ? sorobanAsset : classicAsset, 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, isContractAddress(to) ? sorobanAsset : classicAsset, 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(
|
|
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
|
-
|
|
160
|
-
this.effectAnalyzer.
|
|
161
|
-
|
|
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(
|
|
176
|
-
this.effectAnalyzer.
|
|
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
|
-
* @
|
|
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.
|
|
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'
|
|
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
|
}
|