@stellar-expert/tx-meta-effects-parser 5.1.1 → 5.3.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 +1 -1
- package/src/{asset-supply-processor.js → aggregation/asset-supply-analyzer.js} +3 -3
- package/src/{events-analyzer.js → aggregation/events-analyzer.js} +62 -57
- package/src/aggregation/sac-contract-mapper.js +40 -0
- package/src/{signer-changes-analyzer.js → aggregation/signer-changes-analyzer.js} +1 -1
- package/src/cache/ttl-cache.js +82 -0
- package/src/{tx-effects-analyzer.js → effects-analyzer.js} +50 -10
- package/src/index.js +31 -17
- package/src/{ledger-entry-changes-parser.js → parser/ledger-entry-changes-parser.js} +2 -9
- package/src/{tx-meta-changes-parser.js → parser/tx-meta-changes-parser.js} +1 -1
- package/src/{tx-result-parser.js → parser/tx-result-parser.js} +1 -1
- package/src/{tx-xdr-parser-utils.js → parser/tx-xdr-parser-utils.js} +1 -1
- package/src/analyzer-primitives.js +0 -150
- package/src/effect.js +0 -24
- /package/src/{contract-preimage-encoder.js → parser/contract-preimage-encoder.js} +0 -0
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
const effectTypes = require('
|
|
1
|
+
const effectTypes = require('../effect-types')
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Effect supply computation processor
|
|
5
5
|
*/
|
|
6
|
-
class
|
|
6
|
+
class AssetSupplyAnalyzer {
|
|
7
7
|
/**
|
|
8
8
|
* @param {EffectsAnalyzer} effectsAnalyzer
|
|
9
9
|
*/
|
|
@@ -201,4 +201,4 @@ class CollapsibleMintsAnalyzer {
|
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
module.exports =
|
|
204
|
+
module.exports = AssetSupplyAnalyzer
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const {StrKey} = require('@stellar/stellar-base')
|
|
2
|
-
const
|
|
3
|
-
const {
|
|
4
|
-
const
|
|
2
|
+
const effectTypes = require('../effect-types')
|
|
3
|
+
const {xdrParseScVal, xdrParseAsset, isContractAddress} = require('../parser/tx-xdr-parser-utils')
|
|
4
|
+
const {mapSacContract} = require('./sac-contract-mapper')
|
|
5
5
|
|
|
6
6
|
const EVENT_TYPES = {
|
|
7
7
|
SYSTEM: 0,
|
|
@@ -10,8 +10,11 @@ const EVENT_TYPES = {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
class EventsAnalyzer {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* @param {EffectsAnalyzer} effectsAnalyzer
|
|
15
|
+
*/
|
|
16
|
+
constructor(effectsAnalyzer) {
|
|
17
|
+
this.effectsAnalyzer = effectsAnalyzer
|
|
15
18
|
this.callStack = []
|
|
16
19
|
}
|
|
17
20
|
|
|
@@ -30,7 +33,7 @@ class EventsAnalyzer {
|
|
|
30
33
|
* @private
|
|
31
34
|
*/
|
|
32
35
|
analyzeEvents() {
|
|
33
|
-
const {events} = this.
|
|
36
|
+
const {events} = this.effectsAnalyzer
|
|
34
37
|
if (!events)
|
|
35
38
|
return
|
|
36
39
|
//contract-generated events
|
|
@@ -42,7 +45,7 @@ class EventsAnalyzer {
|
|
|
42
45
|
continue //skip data entries modifications
|
|
43
46
|
const rawData = body.data()
|
|
44
47
|
//add event to the pipeline
|
|
45
|
-
this.
|
|
48
|
+
this.effectsAnalyzer.addEffect({
|
|
46
49
|
type: effectTypes.contractEvent,
|
|
47
50
|
contract: StrKey.encodeContract(evt.contractId()),
|
|
48
51
|
topics,
|
|
@@ -58,7 +61,7 @@ class EventsAnalyzer {
|
|
|
58
61
|
* @private
|
|
59
62
|
*/
|
|
60
63
|
analyzeDiagnosticEvents() {
|
|
61
|
-
const {diagnosticEvents} = this.
|
|
64
|
+
const {diagnosticEvents} = this.effectsAnalyzer
|
|
62
65
|
if (!diagnosticEvents)
|
|
63
66
|
return
|
|
64
67
|
//diagnostic events
|
|
@@ -81,9 +84,11 @@ class EventsAnalyzer {
|
|
|
81
84
|
processDiagnosticEvent(body, type, contractId) {
|
|
82
85
|
const topics = body.topics()
|
|
83
86
|
if (!topics?.length)
|
|
84
|
-
return
|
|
87
|
+
return
|
|
85
88
|
switch (xdrParseScVal(topics[0])) {
|
|
86
89
|
case 'fn_call': // contract call
|
|
90
|
+
if (type !== EVENT_TYPES.DIAGNOSTIC)
|
|
91
|
+
return // skip non-diagnostic events
|
|
87
92
|
const rawArgs = body.data()
|
|
88
93
|
const parsedEvent = {
|
|
89
94
|
type: effectTypes.contractInvoked,
|
|
@@ -97,7 +102,7 @@ class EventsAnalyzer {
|
|
|
97
102
|
parsedEvent.depth = this.callStack.length
|
|
98
103
|
}
|
|
99
104
|
this.callStack.push(parsedEvent)
|
|
100
|
-
this.
|
|
105
|
+
this.effectsAnalyzer.addEffect(parsedEvent)
|
|
101
106
|
break
|
|
102
107
|
case 'fn_return':
|
|
103
108
|
if (type !== EVENT_TYPES.DIAGNOSTIC)
|
|
@@ -110,7 +115,7 @@ class EventsAnalyzer {
|
|
|
110
115
|
}
|
|
111
116
|
break
|
|
112
117
|
//handle standard token contract events
|
|
113
|
-
//see https://github.com/stellar/rs-soroban-sdk/blob/
|
|
118
|
+
//see https://github.com/stellar/rs-soroban-sdk/blob/main/soroban-sdk/src/token.rs
|
|
114
119
|
case 'transfer': {
|
|
115
120
|
if (!matchEventTopicsShape(topics, ['address', 'address', 'str?']))
|
|
116
121
|
return
|
|
@@ -118,7 +123,6 @@ class EventsAnalyzer {
|
|
|
118
123
|
const to = xdrParseScVal(topics[2])
|
|
119
124
|
if (to === from) //self transfer - nothing happens
|
|
120
125
|
return // TODO: need additional checks
|
|
121
|
-
const sorobanAsset = contractId
|
|
122
126
|
const amount = processEventBodyValue(body.data())
|
|
123
127
|
if (!this.matchInvocationEffect(e =>
|
|
124
128
|
(e.function === 'transfer' && matchArrays([from, to, amount], e.args)) ||
|
|
@@ -128,31 +132,31 @@ class EventsAnalyzer {
|
|
|
128
132
|
let classicAsset
|
|
129
133
|
if (topics.length > 3) {
|
|
130
134
|
classicAsset = xdrParseAsset(xdrParseScVal(topics[3]))
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
135
|
+
if (!mapSacContract(this.effectsAnalyzer, contractId, classicAsset)) {
|
|
136
|
+
classicAsset = null //not an SAC event
|
|
137
|
+
}
|
|
134
138
|
}
|
|
135
139
|
if (classicAsset && (classicAsset.includes(from) || classicAsset.includes(to))) { //SAC transfer by asset issuer
|
|
136
140
|
if (classicAsset.includes(from)) {
|
|
137
|
-
this.
|
|
138
|
-
this.
|
|
141
|
+
this.effectsAnalyzer.mint(contractId, amount)
|
|
142
|
+
this.effectsAnalyzer.credit(amount, isContractAddress(to) ? contractId : classicAsset, to)
|
|
139
143
|
}
|
|
140
144
|
if (classicAsset.includes(to)) {
|
|
141
|
-
this.
|
|
142
|
-
this.
|
|
145
|
+
this.effectsAnalyzer.debit(amount, isContractAddress(from) ? contractId : classicAsset, from)
|
|
146
|
+
this.effectsAnalyzer.burn(contractId, amount)
|
|
143
147
|
}
|
|
144
148
|
} else { //other cases
|
|
145
149
|
if (classicAsset && !isContractAddress(from)) { //classic asset bridged to Soroban
|
|
146
|
-
this.
|
|
147
|
-
this.
|
|
150
|
+
this.effectsAnalyzer.burn(classicAsset, amount)
|
|
151
|
+
this.effectsAnalyzer.mint(contractId, amount)
|
|
148
152
|
} else {
|
|
149
|
-
this.
|
|
153
|
+
this.effectsAnalyzer.debit(amount, contractId, from)
|
|
150
154
|
}
|
|
151
155
|
if (classicAsset && !isContractAddress(to)) { //classic asset bridged from Soroban
|
|
152
|
-
this.
|
|
153
|
-
this.
|
|
156
|
+
this.effectsAnalyzer.burn(contractId, amount)
|
|
157
|
+
this.effectsAnalyzer.mint(classicAsset, amount)
|
|
154
158
|
} else {
|
|
155
|
-
this.
|
|
159
|
+
this.effectsAnalyzer.credit(amount, contractId, to)
|
|
156
160
|
}
|
|
157
161
|
}
|
|
158
162
|
|
|
@@ -165,12 +169,15 @@ class EventsAnalyzer {
|
|
|
165
169
|
const amount = processEventBodyValue(body.data())
|
|
166
170
|
if (!this.matchInvocationEffect(e => e.function === 'mint' && matchArrays([to, amount], e.args)))
|
|
167
171
|
return
|
|
168
|
-
this.
|
|
172
|
+
this.effectsAnalyzer.addEffect({
|
|
169
173
|
type: effectTypes.assetMinted,
|
|
170
174
|
asset: contractId,
|
|
171
175
|
amount
|
|
172
176
|
})
|
|
173
|
-
this.
|
|
177
|
+
this.effectsAnalyzer.credit(amount, contractId, to)
|
|
178
|
+
if (topics.length > 3) {
|
|
179
|
+
mapSacContract(this.effectsAnalyzer, contractId, xdrParseAsset(xdrParseScVal(topics[3])))
|
|
180
|
+
}
|
|
174
181
|
}
|
|
175
182
|
break
|
|
176
183
|
case 'burn': {
|
|
@@ -184,8 +191,11 @@ class EventsAnalyzer {
|
|
|
184
191
|
))
|
|
185
192
|
return
|
|
186
193
|
|
|
187
|
-
this.
|
|
188
|
-
this.
|
|
194
|
+
this.effectsAnalyzer.debit(amount, contractId, from)
|
|
195
|
+
this.effectsAnalyzer.burn(contractId, amount)
|
|
196
|
+
if (topics.length > 2) {
|
|
197
|
+
mapSacContract(this.effectsAnalyzer, contractId, xdrParseAsset(xdrParseScVal(topics[2])))
|
|
198
|
+
}
|
|
189
199
|
}
|
|
190
200
|
break
|
|
191
201
|
case 'clawback': {
|
|
@@ -195,46 +205,41 @@ class EventsAnalyzer {
|
|
|
195
205
|
const amount = processEventBodyValue(body.data())
|
|
196
206
|
if (!this.matchInvocationEffect(e => e.function === 'clawback' && matchArrays([from, amount], e.args)))
|
|
197
207
|
return
|
|
198
|
-
this.
|
|
199
|
-
this.
|
|
208
|
+
this.effectsAnalyzer.debit(amount, contractId, from)
|
|
209
|
+
this.effectsAnalyzer.burn(contractId, amount)
|
|
210
|
+
if (topics.length > 3) {
|
|
211
|
+
mapSacContract(this.effectsAnalyzer, contractId, xdrParseAsset(xdrParseScVal(topics[3])))
|
|
212
|
+
}
|
|
200
213
|
}
|
|
201
214
|
break
|
|
202
|
-
|
|
203
|
-
|
|
215
|
+
case 'set_admin': {
|
|
216
|
+
if (!matchEventTopicsShape(topics, ['address', 'str?']))
|
|
217
|
+
return //throw new Error('Non-standard event')
|
|
218
|
+
const currentAdmin = xdrParseScVal(topics[1])
|
|
219
|
+
const newAdmin = processEventBodyValue(body.data())
|
|
220
|
+
if (!this.matchInvocationEffect(e => e.function === 'set_admin' && matchArrays([currentAdmin, newAdmin], [this.effectsAnalyzer.source, e.args])))
|
|
221
|
+
return
|
|
222
|
+
this.effectsAnalyzer.setAdmin(contractId, newAdmin)
|
|
223
|
+
if (topics.length > 2) {
|
|
224
|
+
mapSacContract(this.effectsAnalyzer, contractId, xdrParseAsset(xdrParseScVal(topics[2])))
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
break
|
|
228
|
+
/*case 'approve': { //TODO: think about processing this effect
|
|
204
229
|
if (!matchEventTopicsShape(topics, ['address', 'address', 'str?']))
|
|
205
230
|
throw new Error('Non-standard event')
|
|
206
231
|
const from = xdrParseScVal(topics[1])
|
|
207
232
|
const spender = xdrParseScVal(topics[2])
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
case 'set_authorized': {
|
|
212
|
-
throw new Error('Not implemented')
|
|
213
|
-
//trustlineAuthorizationUpdated
|
|
214
|
-
if (!matchEventTopicsShape(topics, ['address', 'address', 'bool', 'str?']))
|
|
215
|
-
throw new Error('Non-standard event')
|
|
216
|
-
const admin = xdrParseScVal(topics[1])
|
|
217
|
-
const id = xdrParseScVal(topics[2])
|
|
218
|
-
const authorize = xdrParseScVal(topics[3])
|
|
219
|
-
}
|
|
220
|
-
break
|
|
221
|
-
case 'set_admin': {
|
|
222
|
-
throw new Error('Not implemented')
|
|
223
|
-
if (!matchEventTopicsShape(topics, ['address']))
|
|
224
|
-
throw new Error('Non-standard event')
|
|
225
|
-
const prevAdmin = xdrParseScVal(topics[1])
|
|
226
|
-
const newAdmin = processEventBodyValue(topics[2])
|
|
233
|
+
if (topics.length > 3) {
|
|
234
|
+
mapSacContract(this.effectsAnalyzer, contractId, xdrParseAsset(xdrParseScVal(topics[3])))
|
|
235
|
+
}
|
|
227
236
|
}
|
|
228
237
|
break*/
|
|
229
|
-
default:
|
|
230
|
-
//console.log(`Event ` + xdrParseScVal(topics[0]))
|
|
231
|
-
break
|
|
232
238
|
}
|
|
233
|
-
return null
|
|
234
239
|
}
|
|
235
240
|
|
|
236
241
|
matchInvocationEffect(cb) {
|
|
237
|
-
return this.
|
|
242
|
+
return this.effectsAnalyzer.effects.find(e => e.type === effectTypes.contractInvoked && cb(e))
|
|
238
243
|
}
|
|
239
244
|
}
|
|
240
245
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const TtlCache = require('../cache/ttl-cache')
|
|
2
|
+
const {toStellarAsset} = require('../parser/tx-xdr-parser-utils')
|
|
3
|
+
const {contractIdFromAsset} = require('../parser/contract-preimage-encoder')
|
|
4
|
+
|
|
5
|
+
const sacCache = new TtlCache()
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Check and map SAC contract addresses with Classic assets
|
|
9
|
+
* @param {EffectsAnalyzer} effectsAnalyzer
|
|
10
|
+
* @param {String} contractAddress
|
|
11
|
+
* @param {String} classicAsset
|
|
12
|
+
* @return {Boolean}
|
|
13
|
+
*/
|
|
14
|
+
function mapSacContract(effectsAnalyzer, contractAddress, classicAsset) {
|
|
15
|
+
if (!classicAsset)
|
|
16
|
+
return false
|
|
17
|
+
const {network, sacMap} = effectsAnalyzer
|
|
18
|
+
//try to load from cache first
|
|
19
|
+
const fromCache = sacCache.get(contractAddress + network)
|
|
20
|
+
if (!fromCache) {
|
|
21
|
+
const encodedContract = contractIdFromAsset(toStellarAsset(classicAsset), network)
|
|
22
|
+
sacCache.set(encodedContract + network, classicAsset)
|
|
23
|
+
if (encodedContract !== contractAddress)
|
|
24
|
+
return false
|
|
25
|
+
} else if (classicAsset !== fromCache)
|
|
26
|
+
return false //check whether validated contract from cache matches the asset
|
|
27
|
+
if (sacMap) {
|
|
28
|
+
sacMap[contractAddress] = classicAsset
|
|
29
|
+
}
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Dispose SAC cache mapping and release cleanup timers
|
|
35
|
+
*/
|
|
36
|
+
function disposeSacCache() {
|
|
37
|
+
sacCache.dispose()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = {mapSacContract, disposeSacCache}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
class TtlCache {
|
|
2
|
+
/**
|
|
3
|
+
* Create new instance of ClientCache with simple in-memory storage
|
|
4
|
+
* @param {Number} ttl - Uniform time-to-live (in seconds)
|
|
5
|
+
*/
|
|
6
|
+
constructor(ttl = 2 * 60) {
|
|
7
|
+
this.ttl = ttl * 1000
|
|
8
|
+
this.storage = new Map()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @type {Number}
|
|
13
|
+
*/
|
|
14
|
+
ttl
|
|
15
|
+
/**
|
|
16
|
+
* @type {Map<String,{ts:Number,value:*}>}
|
|
17
|
+
* @private
|
|
18
|
+
*/
|
|
19
|
+
storage
|
|
20
|
+
/**
|
|
21
|
+
* @type {Number}
|
|
22
|
+
* @private
|
|
23
|
+
*/
|
|
24
|
+
scheduledCleanup = 0
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Try to retrieve an item from the cache
|
|
28
|
+
* @param {String} key
|
|
29
|
+
* @return {*}
|
|
30
|
+
*/
|
|
31
|
+
get(key) {
|
|
32
|
+
let item = this.storage.get(key)
|
|
33
|
+
if (!item)
|
|
34
|
+
return null
|
|
35
|
+
item.ts = new Date().getTime()
|
|
36
|
+
return item.value
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Add/replace cache object
|
|
41
|
+
* @param {String} key - Unique key
|
|
42
|
+
* @param {*} value - Associated value to store
|
|
43
|
+
*/
|
|
44
|
+
set(key, value) {
|
|
45
|
+
this.storage.set(key, {value, ts: new Date().getTime()})
|
|
46
|
+
this.scheduleCleanup()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @private
|
|
51
|
+
*/
|
|
52
|
+
cleanupApiCache() {
|
|
53
|
+
const {storage, ttl} = this
|
|
54
|
+
const expired = new Date().getTime() - ttl
|
|
55
|
+
for (const [key, item] of storage.entries()) {
|
|
56
|
+
if (item.ts < expired) {
|
|
57
|
+
storage.delete(key)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
this.scheduledCleanup = 0
|
|
61
|
+
this.scheduleCleanup()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @private
|
|
66
|
+
*/
|
|
67
|
+
scheduleCleanup() {
|
|
68
|
+
if (!this.scheduledCleanup && this.storage.size > 0) { //schedule new cleanup if storage is not empty
|
|
69
|
+
this.scheduledCleanup = setTimeout(() => this.cleanupApiCache(), this.ttl + 10)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Stop scheduled cleanup task and free resources
|
|
75
|
+
*/
|
|
76
|
+
dispose() {
|
|
77
|
+
clearTimeout(this.scheduledCleanup)
|
|
78
|
+
this.storage = null
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = TtlCache
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
const {StrKey, hash, xdr, nativeToScVal} = require('@stellar/stellar-base')
|
|
2
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 {
|
|
6
|
-
const {
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const AssetSupplyProcessor = require('./asset-supply-processor')
|
|
3
|
+
const {parseLedgerEntryChanges} = require('./parser/ledger-entry-changes-parser')
|
|
4
|
+
const {xdrParseAsset, xdrParseAccountAddress, xdrParseScVal} = require('./parser/tx-xdr-parser-utils')
|
|
5
|
+
const {analyzeSignerChanges} = require('./aggregation/signer-changes-analyzer')
|
|
6
|
+
const {contractIdFromPreimage} = require('./parser/contract-preimage-encoder')
|
|
7
|
+
const EventsAnalyzer = require('./aggregation/events-analyzer')
|
|
8
|
+
const AssetSupplyAnalyzer = require('./aggregation/asset-supply-analyzer')
|
|
10
9
|
const {UnexpectedTxMetaChangeError, TxMetaEffectParserError} = require('./errors')
|
|
11
10
|
|
|
12
11
|
class EffectsAnalyzer {
|
|
13
|
-
constructor({operation, meta, result, network, events, diagnosticEvents}) {
|
|
12
|
+
constructor({operation, meta, result, network, events, diagnosticEvents, mapSac}) {
|
|
14
13
|
//set execution context
|
|
15
14
|
if (!operation.source)
|
|
16
15
|
throw new TxMetaEffectParserError('Operation source is not explicitly defined')
|
|
@@ -24,6 +23,9 @@ class EffectsAnalyzer {
|
|
|
24
23
|
this.diagnosticEvents = diagnosticEvents
|
|
25
24
|
}
|
|
26
25
|
this.network = network
|
|
26
|
+
if (mapSac) {
|
|
27
|
+
this.sacMap = {}
|
|
28
|
+
}
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/**
|
|
@@ -43,6 +45,11 @@ class EffectsAnalyzer {
|
|
|
43
45
|
* @readonly
|
|
44
46
|
*/
|
|
45
47
|
network
|
|
48
|
+
/**
|
|
49
|
+
* @type {{}}
|
|
50
|
+
* @readonly
|
|
51
|
+
*/
|
|
52
|
+
sacMap
|
|
46
53
|
/**
|
|
47
54
|
* @type {ParsedLedgerEntryMeta[]}
|
|
48
55
|
* @private
|
|
@@ -68,7 +75,7 @@ class EffectsAnalyzer {
|
|
|
68
75
|
isContractCall = false
|
|
69
76
|
|
|
70
77
|
analyze() {
|
|
71
|
-
//find appropriate
|
|
78
|
+
//find appropriate parser method
|
|
72
79
|
const parse = this[this.operation.type]
|
|
73
80
|
if (parse) {
|
|
74
81
|
parse.call(this)
|
|
@@ -80,7 +87,7 @@ class EffectsAnalyzer {
|
|
|
80
87
|
//process Soroban events
|
|
81
88
|
new EventsAnalyzer(this).analyze()
|
|
82
89
|
//calculate minted/burned assets
|
|
83
|
-
new
|
|
90
|
+
new AssetSupplyAnalyzer(this).analyze()
|
|
84
91
|
//process state data changes in the end
|
|
85
92
|
for (const change of this.changes)
|
|
86
93
|
if (change.type === 'contractData') {
|
|
@@ -368,6 +375,15 @@ class EffectsAnalyzer {
|
|
|
368
375
|
restoreFootprint() {
|
|
369
376
|
}
|
|
370
377
|
|
|
378
|
+
setAdmin(contractId, newAdmin) {
|
|
379
|
+
const effect = {
|
|
380
|
+
type: effectTypes.contractUpdated,
|
|
381
|
+
contract: contractId,
|
|
382
|
+
admin: newAdmin
|
|
383
|
+
}
|
|
384
|
+
this.addEffect(effect)
|
|
385
|
+
}
|
|
386
|
+
|
|
371
387
|
processDexOperationEffects() {
|
|
372
388
|
//process trades first
|
|
373
389
|
for (const claimedOffer of this.result.claimedOffers) {
|
|
@@ -897,4 +913,28 @@ function normalizeAddress(address) {
|
|
|
897
913
|
return StrKey.encodeEd25519PublicKey(rawBytes.subarray(0, 32))
|
|
898
914
|
}
|
|
899
915
|
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* @param {String} action
|
|
919
|
+
* @param {String} type
|
|
920
|
+
* @return {String}
|
|
921
|
+
*/
|
|
922
|
+
function encodeSponsorshipEffectName(action, type) {
|
|
923
|
+
let actionKey
|
|
924
|
+
switch (action) {
|
|
925
|
+
case 'created':
|
|
926
|
+
actionKey = 'Created'
|
|
927
|
+
break
|
|
928
|
+
case 'updated':
|
|
929
|
+
actionKey = 'Updated'
|
|
930
|
+
break
|
|
931
|
+
case 'removed':
|
|
932
|
+
actionKey = 'Removed'
|
|
933
|
+
break
|
|
934
|
+
default:
|
|
935
|
+
throw new UnexpectedTxMetaChangeError({action, type})
|
|
936
|
+
}
|
|
937
|
+
return effectTypes[`${type}Sponsorship${actionKey}`]
|
|
938
|
+
}
|
|
939
|
+
|
|
900
940
|
module.exports = {EffectsAnalyzer, processFeeChargedEffect}
|
package/src/index.js
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
const {TransactionBuilder, xdr} = require('@stellar/stellar-base')
|
|
2
|
-
const {processFeeChargedEffect, analyzeOperationEffects, EffectsAnalyzer} = require('./tx-effects-analyzer')
|
|
3
|
-
const {parseTxResult} = require('./tx-result-parser')
|
|
4
|
-
const {parseLedgerEntryChanges} = require('./ledger-entry-changes-parser')
|
|
5
|
-
const {parseTxMetaChanges} = require('./tx-meta-changes-parser')
|
|
6
|
-
const {analyzeSignerChanges} = require('./signer-changes-analyzer')
|
|
7
|
-
const contractPreimageEncoder = require('./contract-preimage-encoder')
|
|
8
|
-
const xdrParserUtils = require('./tx-xdr-parser-utils')
|
|
9
|
-
const effectTypes = require('./effect-types')
|
|
10
2
|
const {TxMetaEffectParserError, UnexpectedTxMetaChangeError} = require('./errors')
|
|
3
|
+
const {processFeeChargedEffect, analyzeOperationEffects, EffectsAnalyzer} = require('./effects-analyzer')
|
|
4
|
+
const {disposeSacCache} = require('./aggregation/sac-contract-mapper')
|
|
5
|
+
const {parseTxResult} = require('./parser/tx-result-parser')
|
|
6
|
+
const {parseLedgerEntryChanges} = require('./parser/ledger-entry-changes-parser')
|
|
7
|
+
const {parseTxMetaChanges} = require('./parser/tx-meta-changes-parser')
|
|
8
|
+
const {analyzeSignerChanges} = require('./aggregation/signer-changes-analyzer')
|
|
9
|
+
const contractPreimageEncoder = require('./parser/contract-preimage-encoder')
|
|
10
|
+
const xdrParserUtils = require('./parser/tx-xdr-parser-utils')
|
|
11
|
+
const effectTypes = require('./effect-types')
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Retrieve effects from transaction execution result metadata
|
|
14
15
|
* @param {String} network - Network passphrase
|
|
15
16
|
* @param {String|Buffer|xdr.TransactionEnvelope} tx - Base64-encoded tx envelope xdr
|
|
16
|
-
* @param {String|Buffer|xdr.TransactionResult} result
|
|
17
|
-
* @param {String|Buffer|xdr.TransactionMeta} meta
|
|
17
|
+
* @param {String|Buffer|xdr.TransactionResult} [result] - Base64-encoded tx envelope result
|
|
18
|
+
* @param {String|Buffer|xdr.TransactionMeta} [meta] - Base64-encoded tx envelope meta
|
|
19
|
+
* @param {Boolean} [mapSac] - Whether to create a map SAC->Asset
|
|
18
20
|
* @return {ParsedTxOperationsMetadata}
|
|
19
21
|
*/
|
|
20
|
-
function parseTxOperationsMeta({network, tx, result, meta}) {
|
|
22
|
+
function parseTxOperationsMeta({network, tx, result, meta, mapSac = false}) {
|
|
21
23
|
if (!network)
|
|
22
24
|
throw new TypeError(`Network passphrase argument is required.`)
|
|
23
25
|
if (typeof network !== 'string')
|
|
@@ -135,9 +137,13 @@ function parseTxOperationsMeta({network, tx, result, meta}) {
|
|
|
135
137
|
const sorobanMeta = metaValue.sorobanMeta()
|
|
136
138
|
params.events = sorobanMeta.events()
|
|
137
139
|
params.diagnosticEvents = sorobanMeta.diagnosticEvents()
|
|
140
|
+
params.mapSac = mapSac
|
|
138
141
|
}
|
|
139
142
|
const analyzer = new EffectsAnalyzer(params)
|
|
140
143
|
operation.effects = analyzer.analyze()
|
|
144
|
+
if (analyzer.sacMap && !isEmptyObject(analyzer.sacMap)) {
|
|
145
|
+
operation.sacMap = analyzer.sacMap
|
|
146
|
+
}
|
|
141
147
|
}
|
|
142
148
|
}
|
|
143
149
|
return res
|
|
@@ -159,13 +165,20 @@ function ensureXdrInputType(value, xdrType) {
|
|
|
159
165
|
return xdrType.fromXDR(value, typeof value === 'string' ? 'base64' : 'raw')
|
|
160
166
|
}
|
|
161
167
|
|
|
168
|
+
function isEmptyObject(obj) {
|
|
169
|
+
for (const key in obj)
|
|
170
|
+
return false
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
|
|
162
174
|
/**
|
|
163
175
|
* @typedef {{}} ParsedTxOperationsMetadata
|
|
164
|
-
* @property {Transaction|FeeBumpTransaction} tx
|
|
165
|
-
* @property {BaseOperation[]} operations
|
|
166
|
-
* @property {Boolean} isEphemeral
|
|
167
|
-
* @property {Boolean} [failed]
|
|
168
|
-
* @property {{}[]} [effects]
|
|
176
|
+
* @property {Transaction|FeeBumpTransaction} tx - Parsed transaction object
|
|
177
|
+
* @property {BaseOperation[]} operations - Transaction operations
|
|
178
|
+
* @property {Boolean} isEphemeral - True for transactions without result metadata
|
|
179
|
+
* @property {Boolean} [failed] - True for transactions failed during on-chain execution
|
|
180
|
+
* @property {{}[]} [effects] - Top-level transaction effects (fee charges and )
|
|
181
|
+
* @property {Object<String,String>} [sacMap] - Optional map of SAC->Asset
|
|
169
182
|
*/
|
|
170
183
|
|
|
171
184
|
module.exports = {
|
|
@@ -176,5 +189,6 @@ module.exports = {
|
|
|
176
189
|
parseTxMetaChanges,
|
|
177
190
|
effectTypes,
|
|
178
191
|
xdrParserUtils,
|
|
179
|
-
contractPreimageEncoder
|
|
192
|
+
contractPreimageEncoder,
|
|
193
|
+
disposeSacCache
|
|
180
194
|
}
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
const {StrKey} = require('@stellar/stellar-base')
|
|
2
|
-
const {
|
|
3
|
-
|
|
4
|
-
xdrParseAccountAddress,
|
|
5
|
-
xdrParseClaimant,
|
|
6
|
-
xdrParsePrice,
|
|
7
|
-
xdrParseSignerKey,
|
|
8
|
-
xdrParseScVal
|
|
9
|
-
} = require('./tx-xdr-parser-utils')
|
|
10
|
-
const {TxMetaEffectParserError} = require('./errors')
|
|
2
|
+
const {TxMetaEffectParserError} = require('../errors')
|
|
3
|
+
const {xdrParseAsset, xdrParseAccountAddress, xdrParseClaimant, xdrParsePrice, xdrParseSignerKey} = require('./tx-xdr-parser-utils')
|
|
11
4
|
|
|
12
5
|
/**
|
|
13
6
|
* @typedef {{}} ParsedLedgerEntryMeta
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const {xdr} = require('@stellar/stellar-base')
|
|
2
2
|
const {xdrParseAccountAddress, xdrParseTradeAtom, xdrParseClaimedOffer, xdrParseAsset} = require('./tx-xdr-parser-utils')
|
|
3
|
-
const {TxMetaEffectParserError} = require('
|
|
3
|
+
const {TxMetaEffectParserError} = require('../errors')
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Parse extra data from operation result
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
const {StrKey} = require('@stellar/stellar-base')
|
|
2
|
-
const effectTypes = require('./effect-types')
|
|
3
|
-
const {UnexpectedTxMetaChangeError} = require('./errors')
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Returns true for AlphaNum4/12 assets adn false otherwise
|
|
7
|
-
* @param {String} asset
|
|
8
|
-
*/
|
|
9
|
-
function isAsset(asset) {
|
|
10
|
-
return asset.includes('-') //lazy check for {code}-{issuer}-{type} format
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Convert value in stroops (Int64 amount) to the normal string representation
|
|
16
|
-
* @param {String|Number|BigInt} valueInStroops
|
|
17
|
-
* @return {String}
|
|
18
|
-
*/
|
|
19
|
-
function fromStroops(valueInStroops) {
|
|
20
|
-
try {
|
|
21
|
-
let parsed = typeof valueInStroops === 'bigint' ?
|
|
22
|
-
valueInStroops :
|
|
23
|
-
BigInt(valueInStroops.toString())
|
|
24
|
-
let negative = false
|
|
25
|
-
if (parsed < 0n) {
|
|
26
|
-
negative = true
|
|
27
|
-
parsed *= -1n
|
|
28
|
-
}
|
|
29
|
-
const int = parsed / 10000000n
|
|
30
|
-
const fract = parsed % 10000000n
|
|
31
|
-
let res = int.toString()
|
|
32
|
-
if (fract) {
|
|
33
|
-
res += '.' + fract.toString().padStart(7, '0')
|
|
34
|
-
}
|
|
35
|
-
if (negative) {
|
|
36
|
-
res = '-' + res
|
|
37
|
-
}
|
|
38
|
-
return trimZeros(res)
|
|
39
|
-
} catch (e) {
|
|
40
|
-
return '0'
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Convert arbitrary stringified amount to int64 representation
|
|
47
|
-
* @param {String|Number} value
|
|
48
|
-
* @return {BigInt}
|
|
49
|
-
*/
|
|
50
|
-
function toStroops(value) {
|
|
51
|
-
if (!value)
|
|
52
|
-
return 0n
|
|
53
|
-
if (typeof value === 'number') {
|
|
54
|
-
value = value.toFixed(7)
|
|
55
|
-
}
|
|
56
|
-
if (typeof value !== 'string' || !/^-?[\d.,]+$/.test(value))
|
|
57
|
-
return 0n //invalid format
|
|
58
|
-
try {
|
|
59
|
-
let [int, decimal = '0'] = value.split('.', 2)
|
|
60
|
-
let negative = false
|
|
61
|
-
if (int.startsWith('-')) {
|
|
62
|
-
negative = true
|
|
63
|
-
int = int.slice(1)
|
|
64
|
-
}
|
|
65
|
-
let res = BigInt(int) * 10000000n + BigInt(decimal.slice(0, 7).padEnd(7, '0'))
|
|
66
|
-
if (negative) {
|
|
67
|
-
res *= -1n
|
|
68
|
-
if (res < -0x8000000000000000n) //overflow
|
|
69
|
-
return 0n
|
|
70
|
-
} else if (res > 0xFFFFFFFFFFFFFFFFn) //overflow
|
|
71
|
-
return 0n
|
|
72
|
-
return res
|
|
73
|
-
} catch (e) {
|
|
74
|
-
return 0n
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Trim trailing fractional zeros from a string amount representation
|
|
80
|
-
* @param {String} value
|
|
81
|
-
* @return {String}
|
|
82
|
-
* @internal
|
|
83
|
-
*/
|
|
84
|
-
function trimZeros(value) {
|
|
85
|
-
const [int, fract] = value.split('.')
|
|
86
|
-
if (!fract)
|
|
87
|
-
return int
|
|
88
|
-
const trimmed = fract.replace(/0+$/, '')
|
|
89
|
-
if (!trimmed.length)
|
|
90
|
-
return int
|
|
91
|
-
return int + '.' + trimmed
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Replace multiplexed addresses with base G addresses
|
|
96
|
-
* @param {String} address
|
|
97
|
-
* @return {String}
|
|
98
|
-
* @internal
|
|
99
|
-
*/
|
|
100
|
-
function normalizeAddress(address) {
|
|
101
|
-
const prefix = address[0]
|
|
102
|
-
if (prefix === 'G')
|
|
103
|
-
return address //lazy check for ed25519 G address
|
|
104
|
-
if (prefix !== 'M')
|
|
105
|
-
throw new TypeError('Expected ED25519 or Muxed address')
|
|
106
|
-
const rawBytes = StrKey.decodeMed25519PublicKey(address)
|
|
107
|
-
return StrKey.encodeEd25519PublicKey(rawBytes.subarray(0, 32))
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* @param {String} action
|
|
112
|
-
* @param {String} type
|
|
113
|
-
* @return {String}
|
|
114
|
-
*/
|
|
115
|
-
function encodeSponsorshipEffectName(action, type) {
|
|
116
|
-
let actionKey
|
|
117
|
-
switch (action) {
|
|
118
|
-
case 'created':
|
|
119
|
-
actionKey = 'Created'
|
|
120
|
-
break
|
|
121
|
-
case 'updated':
|
|
122
|
-
actionKey = 'Updated'
|
|
123
|
-
break
|
|
124
|
-
case 'removed':
|
|
125
|
-
actionKey = 'Removed'
|
|
126
|
-
break
|
|
127
|
-
default:
|
|
128
|
-
throw new UnexpectedTxMetaChangeError({action, type})
|
|
129
|
-
}
|
|
130
|
-
return effectTypes[`${type}Sponsorship${actionKey}`]
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Check if asset issuer is a source account
|
|
135
|
-
* @param {String} account
|
|
136
|
-
* @param {String} asset
|
|
137
|
-
* @return {Boolean}
|
|
138
|
-
*/
|
|
139
|
-
function isIssuer(account, asset) {
|
|
140
|
-
return asset.includes(account)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
module.exports = {
|
|
144
|
-
fromStroops,
|
|
145
|
-
toStroops,
|
|
146
|
-
normalizeAddress,
|
|
147
|
-
encodeSponsorshipEffectName,
|
|
148
|
-
isIssuer,
|
|
149
|
-
isAsset
|
|
150
|
-
}
|
package/src/effect.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
class Effect {
|
|
2
|
-
constructor(type, source) {
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @type {String}
|
|
7
|
-
* @readonly
|
|
8
|
-
*/
|
|
9
|
-
type
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* @type {String}
|
|
13
|
-
* @readonly
|
|
14
|
-
*/
|
|
15
|
-
source
|
|
16
|
-
|
|
17
|
-
toString() {
|
|
18
|
-
return this.toJSON()
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
toJSON() {
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
}
|
|
File without changes
|