@stellar-expert/tx-meta-effects-parser 8.3.2 → 8.4.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/src/index.js CHANGED
@@ -1,233 +1,233 @@
1
- const {TransactionBuilder, xdr} = require('@stellar/stellar-base')
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')
12
-
13
- /**
14
- * Retrieve effects from transaction execution result metadata
15
- * @param {String} network - Network passphrase
16
- * @param {String|Buffer|xdr.TransactionEnvelope} tx - Base64-encoded tx envelope xdr
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
20
- * @param {Boolean} [processSystemEvents] - Emit effects for contract errors and resource stats
21
- * @param {Boolean} [processFailedOpEffects] - Whether to generate operation effects for failed/rejected transactions
22
- * @param {Boolean} [processMetrics] - Process invocation metrics emitted by Soroban
23
- * @param {Number} [protocol] - Specific Stellar protocol version for the executed transaction
24
- * @return {ParsedTxOperationsMetadata}
25
- */
26
- function parseTxOperationsMeta({
27
- network,
28
- tx,
29
- result,
30
- meta,
31
- mapSac = false,
32
- processSystemEvents = false,
33
- processFailedOpEffects = false,
34
- processMetrics,
35
- protocol
36
- }) {
37
- if (!network)
38
- throw new TypeError(`Network passphrase argument is required.`)
39
- if (typeof network !== 'string')
40
- throw new TypeError(`Invalid network passphrase: "${network}".`)
41
- if (!tx)
42
- throw new TypeError(`Transaction envelope argument is required.`)
43
- if (processMetrics !== false)
44
- processMetrics = true
45
- const isEphemeral = !meta
46
- //parse tx, result, and meta xdr
47
- try {
48
- tx = ensureXdrInputType(tx, xdr.TransactionEnvelope)
49
- } catch (e) {
50
- throw new TxMetaEffectParserError('Invalid transaction envelope XDR. ' + e.message)
51
- }
52
- if (!isEphemeral) {
53
- try {
54
- result = ensureXdrInputType(result, xdr.TransactionResult)
55
- } catch (e) {
56
- try {
57
- const pair = ensureXdrInputType(result, xdr.TransactionResultPair)
58
- result = pair.result()
59
- } catch {
60
- throw new TxMetaEffectParserError('Invalid transaction result XDR. ' + e.message)
61
- }
62
- }
63
- }
64
- tx = TransactionBuilder.fromXDR(tx, network)
65
-
66
- let parsedTx = tx
67
- let parsedResult = result
68
-
69
- const isFeeBump = !!parsedTx.innerTransaction
70
- let feeBumpSuccess
71
-
72
- const res = {
73
- tx,
74
- isEphemeral
75
- }
76
-
77
- //take inner transaction if parsed tx is a fee bump tx
78
- if (isFeeBump) {
79
- parsedTx = parsedTx.innerTransaction
80
- if (parsedTx.innerTransaction)
81
- throw new TxMetaEffectParserError('Failed to process FeeBumpTransaction wrapped with another FeeBumpTransaction')
82
- if (!isEphemeral) {
83
- parsedResult = result.result().innerResultPair().result()
84
- feeBumpSuccess = parsedResult.result().switch().value >= 0
85
- }
86
- }
87
-
88
- //normalize operation source and effects container
89
- if (parsedTx.operations) {
90
- res.operations = parsedTx.operations
91
-
92
- for (const op of parsedTx.operations) {
93
- if (!op.source) {
94
- op.source = parsedTx.source
95
- }
96
- op.effects = []
97
- }
98
- }
99
-
100
- res.effects = []
101
-
102
- if (isEphemeral)
103
- return res //do not parse meta for unsubmitted/rejected transactions
104
-
105
- //process fee charge
106
- const feeEffect = processFeeChargedEffect(tx, tx.feeSource || parsedTx.source, result.feeCharged().toString(), isFeeBump)
107
- res.effects.push(feeEffect)
108
-
109
- //check execution result
110
- const {success, opResults} = parseTxResult(parsedResult)
111
- if (!success || isFeeBump && !feeBumpSuccess) {
112
- res.failed = true
113
- if (!processFailedOpEffects)
114
- return res
115
- }
116
-
117
- //retrieve operations result metadata
118
- try {
119
- meta = ensureXdrInputType(meta, xdr.TransactionMeta)
120
- } catch (e) {
121
- throw new TxMetaEffectParserError('Invalid transaction metadata XDR. ' + e.message)
122
- }
123
- let feeSource = feeEffect.source
124
- if (feeSource.startsWith('M')) {
125
- feeSource = xdrParserUtils.retrieveBaseMuxedAddress(feeSource)
126
- }
127
- //add tx-level effects
128
- for (const {before, after} of parseTxMetaChanges(meta)) {
129
- if (before.entry !== 'account')
130
- throw new UnexpectedTxMetaChangeError({type: before.entry, action: 'update'})
131
- for (const effect of analyzeSignerChanges(before, after)) {
132
- effect.source = (before || after).address
133
- res.effects.push(effect)
134
- }
135
- if (feeSource === after.address) {
136
- feeEffect.balance = after.balance
137
- }
138
- if (isFeeBump && protocol === 20 && before.balance !== after.balance) { //bump fee calculation bug in protocol v20
139
- const currentFee = BigInt(feeEffect.charged)
140
- const diff = BigInt(after.balance) - BigInt(before.balance)
141
- if (diff < currentFee) { // do not allow negative fee
142
- feeEffect.charged = (currentFee - diff).toString()
143
- }
144
- }
145
- }
146
- const metaValue = meta.value()
147
- const opMeta = metaValue.operations()
148
- const isV4Meta = meta.arm() === 'v4'
149
-
150
- //analyze operation effects for each operation
151
- for (let i = 0; i < parsedTx.operations.length; i++) {
152
- const operation = parsedTx.operations[i]
153
- if (success || processFailedOpEffects) {
154
- const params = {
155
- network,
156
- operation,
157
- meta: opMeta[i]?.changes() || [],
158
- result: opResults[i],
159
- processFailedOpEffects,
160
- processMetrics
161
- }
162
- const isSorobanInvocation = operation.type === 'invokeHostFunction'
163
- //only for Soroban contract invocation
164
- if (isSorobanInvocation) {
165
- const {sorobanMeta} = metaValue._attributes
166
- if (sorobanMeta) {
167
- if (sorobanMeta.events) {
168
- params.events = sorobanMeta.events()
169
- }
170
- if (sorobanMeta.diagnosticEvents) {
171
- params.diagnosticEvents = sorobanMeta.diagnosticEvents()
172
- }
173
- params.processSystemEvents = processSystemEvents
174
- }
175
- if (isV4Meta) {
176
- params.diagnosticEvents = metaValue.diagnosticEvents()
177
- const invocationOp = metaValue.operations()[0]
178
- if (invocationOp) {
179
- params.events = invocationOp.events()
180
- }
181
- }
182
- params.mapSac = mapSac
183
- }
184
- const analyzer = new EffectsAnalyzer(params)
185
- operation.effects = analyzer.analyze()
186
- if (analyzer.sacMap && analyzer.sacMap.size > 0) {
187
- operation.sacMap = analyzer.sacMap
188
- }
189
- if (isSorobanInvocation) {
190
- analyzer.addFeeMetric(metaValue)
191
- }
192
- }
193
- }
194
- return res
195
- }
196
-
197
- /**
198
- * Convert base64/raw XDR representation to XDR type
199
- * @param {String|Buffer|Uint8Array|xdrType} value
200
- * @param xdrType
201
- * @return {xdrType|*}
202
- * @internal
203
- */
204
- function ensureXdrInputType(value, xdrType) {
205
- if (value?.toXDR) // duck-typing check XDR types
206
- return value
207
-
208
- if (!value || (typeof value !== 'string' && !(value instanceof Uint8Array)))
209
- throw new TypeError(`Invalid input format. Expected xdr.${xdrType.name} (raw, buffer, or bas64-encoded).`)
210
- return xdrType.fromXDR(value, typeof value === 'string' ? 'base64' : 'raw')
211
- }
212
-
213
- /**
214
- * @typedef {{}} ParsedTxOperationsMetadata
215
- * @property {Transaction|FeeBumpTransaction} tx - Parsed transaction object
216
- * @property {BaseOperation[]} operations - Transaction operations
217
- * @property {Boolean} isEphemeral - True for transactions without result metadata
218
- * @property {Boolean} [failed] - True for transactions failed during on-chain execution
219
- * @property {{}[]} [effects] - Top-level transaction effects (fee charges and )
220
- * @property {Object<String,String>} [sacMap] - Optional map of SAC->Asset
221
- */
222
-
223
- module.exports = {
224
- parseTxOperationsMeta,
225
- parseTxResult,
226
- analyzeOperationEffects,
227
- parseLedgerEntryChanges,
228
- parseTxMetaChanges,
229
- effectTypes,
230
- xdrParserUtils,
231
- contractPreimageEncoder,
232
- disposeSacCache
1
+ const {TransactionBuilder, xdr} = require('@stellar/stellar-base')
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')
12
+
13
+ /**
14
+ * Retrieve effects from transaction execution result metadata
15
+ * @param {String} network - Network passphrase
16
+ * @param {String|Buffer|xdr.TransactionEnvelope} tx - Base64-encoded tx envelope xdr
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
20
+ * @param {Boolean} [processSystemEvents] - Emit effects for contract errors and resource stats
21
+ * @param {Boolean} [processFailedOpEffects] - Whether to generate operation effects for failed/rejected transactions
22
+ * @param {Boolean} [processMetrics] - Process invocation metrics emitted by Soroban
23
+ * @param {Number} [protocol] - Specific Stellar protocol version for the executed transaction
24
+ * @return {ParsedTxOperationsMetadata}
25
+ */
26
+ function parseTxOperationsMeta({
27
+ network,
28
+ tx,
29
+ result,
30
+ meta,
31
+ mapSac = false,
32
+ processSystemEvents = false,
33
+ processFailedOpEffects = false,
34
+ processMetrics,
35
+ protocol
36
+ }) {
37
+ if (!network)
38
+ throw new TypeError(`Network passphrase argument is required.`)
39
+ if (typeof network !== 'string')
40
+ throw new TypeError(`Invalid network passphrase: "${network}".`)
41
+ if (!tx)
42
+ throw new TypeError(`Transaction envelope argument is required.`)
43
+ if (processMetrics !== false)
44
+ processMetrics = true
45
+ const isEphemeral = !meta
46
+ //parse tx, result, and meta xdr
47
+ try {
48
+ tx = ensureXdrInputType(tx, xdr.TransactionEnvelope)
49
+ } catch (e) {
50
+ throw new TxMetaEffectParserError('Invalid transaction envelope XDR. ' + e.message)
51
+ }
52
+ if (!isEphemeral) {
53
+ try {
54
+ result = ensureXdrInputType(result, xdr.TransactionResult)
55
+ } catch (e) {
56
+ try {
57
+ const pair = ensureXdrInputType(result, xdr.TransactionResultPair)
58
+ result = pair.result()
59
+ } catch {
60
+ throw new TxMetaEffectParserError('Invalid transaction result XDR. ' + e.message)
61
+ }
62
+ }
63
+ }
64
+ tx = TransactionBuilder.fromXDR(tx, network)
65
+
66
+ let parsedTx = tx
67
+ let parsedResult = result
68
+
69
+ const isFeeBump = !!parsedTx.innerTransaction
70
+ let feeBumpSuccess
71
+
72
+ const res = {
73
+ tx,
74
+ isEphemeral
75
+ }
76
+
77
+ //take inner transaction if parsed tx is a fee bump tx
78
+ if (isFeeBump) {
79
+ parsedTx = parsedTx.innerTransaction
80
+ if (parsedTx.innerTransaction)
81
+ throw new TxMetaEffectParserError('Failed to process FeeBumpTransaction wrapped with another FeeBumpTransaction')
82
+ if (!isEphemeral) {
83
+ parsedResult = result.result().innerResultPair().result()
84
+ feeBumpSuccess = parsedResult.result().switch().value >= 0
85
+ }
86
+ }
87
+
88
+ //normalize operation source and effects container
89
+ if (parsedTx.operations) {
90
+ res.operations = parsedTx.operations
91
+
92
+ for (const op of parsedTx.operations) {
93
+ if (!op.source) {
94
+ op.source = parsedTx.source
95
+ }
96
+ op.effects = []
97
+ }
98
+ }
99
+
100
+ res.effects = []
101
+
102
+ if (isEphemeral)
103
+ return res //do not parse meta for unsubmitted/rejected transactions
104
+
105
+ //process fee charge
106
+ const feeEffect = processFeeChargedEffect(tx, tx.feeSource || parsedTx.source, result.feeCharged().toString(), isFeeBump)
107
+ res.effects.push(feeEffect)
108
+
109
+ //check execution result
110
+ const {success, opResults} = parseTxResult(parsedResult)
111
+ if (!success || isFeeBump && !feeBumpSuccess) {
112
+ res.failed = true
113
+ if (!processFailedOpEffects)
114
+ return res
115
+ }
116
+
117
+ //retrieve operations result metadata
118
+ try {
119
+ meta = ensureXdrInputType(meta, xdr.TransactionMeta)
120
+ } catch (e) {
121
+ throw new TxMetaEffectParserError('Invalid transaction metadata XDR. ' + e.message)
122
+ }
123
+ let feeSource = feeEffect.source
124
+ if (feeSource.startsWith('M')) {
125
+ feeSource = xdrParserUtils.retrieveBaseMuxedAddress(feeSource)
126
+ }
127
+ //add tx-level effects
128
+ for (const {before, after} of parseTxMetaChanges(meta)) {
129
+ if (before.entry !== 'account')
130
+ throw new UnexpectedTxMetaChangeError({type: before.entry, action: 'update'})
131
+ for (const effect of analyzeSignerChanges(before, after)) {
132
+ effect.source = (before || after).address
133
+ res.effects.push(effect)
134
+ }
135
+ if (feeSource === after.address) {
136
+ feeEffect.balance = after.balance
137
+ }
138
+ if (isFeeBump && protocol === 20 && before.balance !== after.balance) { //bump fee calculation bug in protocol v20
139
+ const currentFee = BigInt(feeEffect.charged)
140
+ const diff = BigInt(after.balance) - BigInt(before.balance)
141
+ if (diff < currentFee) { // do not allow negative fee
142
+ feeEffect.charged = (currentFee - diff).toString()
143
+ }
144
+ }
145
+ }
146
+ const metaValue = meta.value()
147
+ const opMeta = metaValue.operations()
148
+ const isV4Meta = meta.arm() === 'v4'
149
+
150
+ //analyze operation effects for each operation
151
+ for (let i = 0; i < parsedTx.operations.length; i++) {
152
+ const operation = parsedTx.operations[i]
153
+ if (success || processFailedOpEffects) {
154
+ const params = {
155
+ network,
156
+ operation,
157
+ meta: opMeta[i]?.changes() || [],
158
+ result: opResults[i],
159
+ processFailedOpEffects,
160
+ processMetrics
161
+ }
162
+ const isSorobanInvocation = operation.type === 'invokeHostFunction'
163
+ //only for Soroban contract invocation
164
+ if (isSorobanInvocation) {
165
+ const {sorobanMeta} = metaValue._attributes
166
+ if (sorobanMeta) {
167
+ if (sorobanMeta.events) {
168
+ params.events = sorobanMeta.events()
169
+ }
170
+ if (sorobanMeta.diagnosticEvents) {
171
+ params.diagnosticEvents = sorobanMeta.diagnosticEvents()
172
+ }
173
+ params.processSystemEvents = processSystemEvents
174
+ }
175
+ if (isV4Meta) {
176
+ params.diagnosticEvents = metaValue.diagnosticEvents()
177
+ const invocationOp = metaValue.operations()[0]
178
+ if (invocationOp) {
179
+ params.events = invocationOp.events()
180
+ }
181
+ }
182
+ params.mapSac = mapSac
183
+ }
184
+ const analyzer = new EffectsAnalyzer(params)
185
+ operation.effects = analyzer.analyze()
186
+ if (analyzer.sacMap && analyzer.sacMap.size > 0) {
187
+ operation.sacMap = analyzer.sacMap
188
+ }
189
+ if (isSorobanInvocation) {
190
+ analyzer.addFeeMetric(metaValue)
191
+ }
192
+ }
193
+ }
194
+ return res
195
+ }
196
+
197
+ /**
198
+ * Convert base64/raw XDR representation to XDR type
199
+ * @param {String|Buffer|Uint8Array|xdrType} value
200
+ * @param xdrType
201
+ * @return {xdrType|*}
202
+ * @internal
203
+ */
204
+ function ensureXdrInputType(value, xdrType) {
205
+ if (value?.toXDR) // duck-typing check XDR types
206
+ return value
207
+
208
+ if (!value || (typeof value !== 'string' && !(value instanceof Uint8Array)))
209
+ throw new TypeError(`Invalid input format. Expected xdr.${xdrType.name} (raw, buffer, or bas64-encoded).`)
210
+ return xdrType.fromXDR(value, typeof value === 'string' ? 'base64' : 'raw')
211
+ }
212
+
213
+ /**
214
+ * @typedef {{}} ParsedTxOperationsMetadata
215
+ * @property {Transaction|FeeBumpTransaction} tx - Parsed transaction object
216
+ * @property {BaseOperation[]} operations - Transaction operations
217
+ * @property {Boolean} isEphemeral - True for transactions without result metadata
218
+ * @property {Boolean} [failed] - True for transactions failed during on-chain execution
219
+ * @property {{}[]} [effects] - Top-level transaction effects (fee charges and )
220
+ * @property {Object<String,String>} [sacMap] - Optional map of SAC->Asset
221
+ */
222
+
223
+ module.exports = {
224
+ parseTxOperationsMeta,
225
+ parseTxResult,
226
+ analyzeOperationEffects,
227
+ parseLedgerEntryChanges,
228
+ parseTxMetaChanges,
229
+ effectTypes,
230
+ xdrParserUtils,
231
+ contractPreimageEncoder,
232
+ disposeSacCache
233
233
  }