@stellar-expert/tx-meta-effects-parser 5.7.1 → 6.0.1

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.7.1",
3
+ "version": "6.0.1",
4
4
  "description": "Low-level effects parser for Stellar transaction results and meta XDR",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -81,6 +81,7 @@ const effectTypes = {
81
81
  signerSponsorshipRemoved: 'signerSponsorshipRemoved',
82
82
 
83
83
  contractCodeUploaded: 'contractCodeUploaded',
84
+ contractCodeRemoved: 'contractCodeRemoved',
84
85
 
85
86
  contractCreated: 'contractCreated',
86
87
  contractUpdated: 'contractUpdated',
@@ -93,7 +94,9 @@ const effectTypes = {
93
94
  contractDataRemoved: 'contractDataRemoved',
94
95
 
95
96
  contractEvent: 'contractEvent',
96
- contractMetrics: 'contractMetrics'
97
+ contractMetrics: 'contractMetrics',
98
+
99
+ setTtl: 'setTtl'
97
100
  }
98
101
 
99
102
  module.exports = effectTypes
@@ -8,6 +8,7 @@ const EventsAnalyzer = require('./aggregation/events-analyzer')
8
8
  const AssetSupplyAnalyzer = require('./aggregation/asset-supply-analyzer')
9
9
  const {mapSacContract} = require('./aggregation/sac-contract-mapper')
10
10
  const {UnexpectedTxMetaChangeError, TxMetaEffectParserError} = require('./errors')
11
+ const {generateContractCodeEntryHash} = require('./parser/ledger-key')
11
12
 
12
13
  class EffectsAnalyzer {
13
14
  constructor({operation, meta, result, network, events, diagnosticEvents, mapSac, processSystemEvents, processFailedOpEffects}) {
@@ -95,16 +96,16 @@ class EffectsAnalyzer {
95
96
  if (parse) {
96
97
  parse.call(this)
97
98
  }
99
+ //process Soroban events
100
+ new EventsAnalyzer(this).analyze()
101
+ //process state data changes in the end
102
+ this.processStateChanges()
98
103
  //process ledger entry changes
99
104
  this.processChanges()
100
105
  //handle effects that are processed indirectly
101
106
  this.processSponsorshipEffects()
102
- //process Soroban events
103
- new EventsAnalyzer(this).analyze()
104
107
  //calculate minted/burned assets
105
108
  new AssetSupplyAnalyzer(this).analyze()
106
- //process state data changes in the end
107
- this.processStateChanges()
108
109
  return this.effects
109
110
  }
110
111
 
@@ -311,10 +312,10 @@ class EffectsAnalyzer {
311
312
 
312
313
  liquidityPoolDeposit() {
313
314
  const {liquidityPoolId} = this.operation
314
- const {
315
- before,
316
- after
317
- } = this.changes.find(ch => ch.type === 'liquidityPool' && ch.action === 'updated' && ch.after.pool === liquidityPoolId)
315
+ const poolUpdatedChanges = this.changes.find(ch => ch.type === 'liquidityPool' && ch.action === 'updated' && ch.after.pool === liquidityPoolId)
316
+ if (!poolUpdatedChanges) //tx failed
317
+ return
318
+ const {before, after} = poolUpdatedChanges
318
319
  this.addEffect({
319
320
  type: effectTypes.liquidityPoolDeposited,
320
321
  pool: this.operation.liquidityPoolId,
@@ -358,13 +359,16 @@ class EffectsAnalyzer {
358
359
  this.addEffect(effect)
359
360
  }
360
361
  break
361
- case 'wasm':
362
+ case 'wasm': {
363
+ const codeHash = hash(value)
362
364
  this.addEffect({
363
365
  type: effectTypes.contractCodeUploaded,
364
366
  wasm: value.toString('base64'),
365
- wasmHash: hash(value).toString('hex')
367
+ wasmHash: codeHash.toString('hex'),
368
+ keyHash: generateContractCodeEntryHash(codeHash)
366
369
  })
367
370
  break
371
+ }
368
372
  case 'createContract':
369
373
  const preimage = value.contractIdPreimage()
370
374
  const executable = value.executable()
@@ -398,7 +402,7 @@ class EffectsAnalyzer {
398
402
  default:
399
403
  throw new TxMetaEffectParserError('Unknown contract type: ' + executableType)
400
404
  }
401
- this.addEffect(effect)
405
+ this.addEffect(effect, 0)
402
406
  break
403
407
  default:
404
408
  throw new TxMetaEffectParserError('Unknown host function call type: ' + func.arm())
@@ -765,7 +769,7 @@ class EffectsAnalyzer {
765
769
  const balanceEffects = this.effects.filter(e => (e.type === effectTypes.accountCredited || e.type === effectTypes.accountDebited) && e.source === account && e.asset === effect.owner)
766
770
  if (balanceEffects.length !== 1) //we can set balance only when we found 1-1 mapping, if there are several balance changes, we can't establish balance relation
767
771
  return
768
- if (effect.type === effectTypes.contractDataRemoved) { //balance completely removed
772
+ if (effect.type === effectTypes.contractDataRemoved) { //balance completely removed - this may be a reversible operation if the balance simply expired
769
773
  balanceEffects[0].balance = '0'
770
774
  return
771
775
  }
@@ -782,8 +786,8 @@ class EffectsAnalyzer {
782
786
  processContractChanges({action, before, after}) {
783
787
  if (action !== 'created' && action !== 'updated')
784
788
  throw new UnexpectedTxMetaChangeError({type: 'contract', action})
785
- const {kind, contract} = after
786
- const effect = {
789
+ const {kind, owner: contract, keyHash} = after
790
+ let effect = {
787
791
  type: effectTypes.contractCreated,
788
792
  contract,
789
793
  kind
@@ -793,33 +797,38 @@ class EffectsAnalyzer {
793
797
  effect.asset = after.asset
794
798
  break
795
799
  case 'wasm':
796
- effect.wasmHash = after.hash
800
+ effect.wasmHash = after.wasmHash
797
801
  break
798
802
  default:
799
803
  throw new TxMetaEffectParserError('Unexpected contract type: ' + kind)
800
804
  }
801
805
  if (action === 'created') {
802
- if (this.effects.some(e => e.contract === contract))
803
- return //skip contract creation effects processed by top-level createContract operation call
806
+ if (this.effects.some(e => e.type === effectTypes.contractCreated && e.contract === contract)) {
807
+ effect = undefined //skip contract creation effects processed by top-level createContract operation call
808
+ }
804
809
  } else if (action === 'updated') {
805
810
  effect.type = effectTypes.contractUpdated
806
- effect.prevWasmHash = before.hash
807
- if (before.storage?.length || after.storage?.length) {
808
- this.processInstanceDataChanges(before, after)
811
+ effect.prevWasmHash = before.wasmHash
812
+ if (before.wasmHash === after.wasmHash) {//skip if hash unchanged
813
+ effect = undefined
809
814
  }
810
- if (before.hash === after.hash) //skip if hash unchanged
811
- return
812
815
  }
813
- this.addEffect(effect)
816
+ if (effect) {
817
+ this.addEffect(effect, effect.type === effectTypes.contractCreated ? 0 : undefined)
818
+ }
819
+ if (before?.storage?.length || after?.storage?.length) {
820
+ this.processInstanceDataChanges(before, after)
821
+ }
814
822
  }
815
823
 
816
824
  processContractStateEntryChanges({action, before, after}) {
817
- const {owner, key, durability} = after || before
825
+ const {owner, key, durability, keyHash} = after || before
818
826
  const effect = {
819
827
  type: '',
820
828
  owner,
829
+ key,
821
830
  durability,
822
- key
831
+ keyHash
823
832
  }
824
833
  switch (action) {
825
834
  case 'created':
@@ -842,9 +851,27 @@ class EffectsAnalyzer {
842
851
  this.processContractBalance(effect)
843
852
  }
844
853
 
854
+ processContractCodeChanges({type, action, before, after}) {
855
+ const {hash, keyHash} = after || before
856
+ switch (action) {
857
+ case 'created':
858
+ break //processed separately
859
+ case 'updated':
860
+ break //it doesn't change the state
861
+ case 'removed':
862
+ const effect = {
863
+ type: effectTypes.contractCodeRemoved,
864
+ wasmHash: hash,
865
+ keyHash
866
+ }
867
+ this.addEffect(effect)
868
+ break
869
+ }
870
+ }
871
+
845
872
  processInstanceDataChanges(before, after) {
846
- const storageBefore = before.storage || []
847
- const storageAfter = [...(after.storage || [])]
873
+ const storageBefore = before?.storage || []
874
+ const storageAfter = [...(after?.storage || [])]
848
875
  for (const {key, val} of storageBefore) {
849
876
  let newVal
850
877
  for (let i = 0; i < storageAfter.length; i++) {
@@ -858,7 +885,7 @@ class EffectsAnalyzer {
858
885
  if (newVal === undefined) { //removed
859
886
  const effect = {
860
887
  type: effectTypes.contractDataRemoved,
861
- owner: after.contract || before.contract,
888
+ owner: after?.owner || before.owner,
862
889
  key,
863
890
  prevValue: val,
864
891
  durability: 'instance'
@@ -871,7 +898,7 @@ class EffectsAnalyzer {
871
898
 
872
899
  const effect = {
873
900
  type: effectTypes.contractDataUpdated,
874
- owner: after.contract || before.contract,
901
+ owner: after?.owner || before.owner,
875
902
  key,
876
903
  value: newVal,
877
904
  prevValue: val,
@@ -883,7 +910,7 @@ class EffectsAnalyzer {
883
910
  for (const {key, val} of storageAfter) {
884
911
  const effect = {
885
912
  type: effectTypes.contractDataCreated,
886
- owner: after.contract || before.contract,
913
+ owner: after?.owner || before.owner,
887
914
  key,
888
915
  value: val,
889
916
  durability: 'instance'
@@ -892,6 +919,27 @@ class EffectsAnalyzer {
892
919
  }
893
920
  }
894
921
 
922
+ processTtlChanges({action, before, after}) {
923
+ /*if (action === 'removed')
924
+ throw new UnexpectedTxMetaChangeError({type: 'ttl', action})*/
925
+ const {keyHash, ttl} = after || before
926
+ const stateEffect = this.effects.find(e => e.keyHash === keyHash && e.type !== effectTypes.setTtl)
927
+ const effect = {
928
+ type: effectTypes.setTtl,
929
+ keyHash,
930
+ ttl
931
+ }
932
+ if (stateEffect) {
933
+ if (stateEffect.type.includes('contractCode')) {
934
+ effect.kind = 'contractCode'
935
+ } else if (stateEffect.type.includes('contractData')) {
936
+ effect.kind = 'contractData'
937
+ effect.owner = stateEffect.owner
938
+ }
939
+ }
940
+ this.addEffect(effect)
941
+ }
942
+
895
943
  processChanges() {
896
944
  for (const change of this.changes)
897
945
  switch (change.type) {
@@ -914,10 +962,15 @@ class EffectsAnalyzer {
914
962
  this.processDataEntryChanges(change)
915
963
  break
916
964
  case 'contractData':
917
- //this.processContractDataChanges(change)
965
+ if (change.before?.kind || change.after?.kind) {
966
+ this.processContractChanges(change)
967
+ }
968
+ break
969
+ case 'contractCode':
970
+ this.processContractCodeChanges(change)
918
971
  break
919
- case 'contract':
920
- this.processContractChanges(change)
972
+ case 'ttl':
973
+ this.processTtlChanges(change)
921
974
  break
922
975
  default:
923
976
  throw new UnexpectedTxMetaChangeError(change)
package/src/index.js CHANGED
@@ -26,7 +26,7 @@ function parseTxOperationsMeta({network, tx, result, meta, mapSac = false, proce
26
26
  if (!network)
27
27
  throw new TypeError(`Network passphrase argument is required.`)
28
28
  if (typeof network !== 'string')
29
- throw new TypeError(`Invalid network passphrase or identifier: "${network}".`)
29
+ throw new TypeError(`Invalid network passphrase: "${network}".`)
30
30
  if (!tx)
31
31
  throw new TypeError(`Transaction envelope argument is required.`)
32
32
  const isEphemeral = !meta
@@ -1,10 +1,11 @@
1
1
  const {StrKey} = require('@stellar/stellar-base')
2
- const {TxMetaEffectParserError} = require('../errors')
2
+ const {TxMetaEffectParserError, UnexpectedTxMetaChangeError} = require('../errors')
3
3
  const {xdrParseAsset, xdrParseAccountAddress, xdrParseClaimant, xdrParsePrice, xdrParseSignerKey} = require('./tx-xdr-parser-utils')
4
+ const {generateContractStateEntryHash, generateContractCodeEntryHash} = require('./ledger-key')
4
5
 
5
6
  /**
6
7
  * @typedef {{}} ParsedLedgerEntryMeta
7
- * @property {'account'|'trustline'|'offer'|'data'|'liquidityPool'|'claimableBalance'|'contractData'|'contractCode'} type - Ledger entry type
8
+ * @property {'account'|'trustline'|'offer'|'data'|'liquidityPool'|'claimableBalance'|'contractData'|'contractCode'|'ttl'} type - Ledger entry type
8
9
  * @property {'created'|'updated'|'removed'} action - Ledger modification action
9
10
  * @property {{}} before - Ledger entry state before changes applied
10
11
  * @property {{}} after - Ledger entry state after changes application
@@ -19,35 +20,39 @@ function parseLedgerEntryChanges(ledgerEntryChanges) {
19
20
  let state
20
21
  for (let i = 0; i < ledgerEntryChanges.length; i++) {
21
22
  const entry = ledgerEntryChanges[i]
22
- const actionType = entry.arm()
23
-
24
- const stateData = parseEntry(entry.value(), actionType)
23
+ const action = entry._arm
24
+ const stateData = parseEntry(entry, action)
25
25
  if (stateData === undefined)
26
26
  continue
27
- const change = {action: actionType}
28
- switch (actionType) {
27
+ const change = {action}
28
+ const type = entry._value._arm
29
+ switch (action) {
29
30
  case 'state':
30
31
  state = stateData
31
32
  continue
32
33
  case 'created':
34
+ if (type === 'contractCode')
35
+ continue //processed in operation handler
33
36
  change.before = null
34
37
  change.after = stateData
35
38
  change.type = stateData.entry
36
39
  break
37
40
  case 'updated':
41
+ if (type === 'contractCode')
42
+ throw new UnexpectedTxMetaChangeError({type, action})
38
43
  change.before = state
39
44
  change.after = stateData
40
45
  change.type = stateData.entry
41
46
  break
42
47
  case 'removed':
43
- if (!state && entry._value._arm === 'ttl')
48
+ if (!state && type === 'ttl')
44
49
  continue //skip expiration processing for now
45
50
  change.before = state
46
51
  change.after = null
47
52
  change.type = state.entry
48
53
  break
49
54
  default:
50
- throw new TxMetaEffectParserError(`Unknown change entry type: ${actionType}`)
55
+ throw new TxMetaEffectParserError(`Unknown change entry type: ${action}`)
51
56
  }
52
57
  changes.push(change)
53
58
  state = null
@@ -57,12 +62,13 @@ function parseLedgerEntryChanges(ledgerEntryChanges) {
57
62
 
58
63
  function parseEntry(entry, actionType) {
59
64
  if (actionType === 'removed')
60
- return null //parseEntryData(entry)
61
- const parsed = parseEntryData(entry.data())
65
+ return null
66
+ const value = entry.value()
67
+ const parsed = parseEntryData(value.data())
62
68
  if (parsed === null)
63
69
  return null
64
70
  //parsed.modified = entry.lastModifiedLedgerSeq()
65
- return parseLedgerEntryExt(parsed, entry)
71
+ return parseLedgerEntryExt(parsed, value)
66
72
  }
67
73
 
68
74
  function parseEntryData(data) {
@@ -85,9 +91,9 @@ function parseEntryData(data) {
85
91
  case 'contractData':
86
92
  return parseContractData(data)
87
93
  case 'contractCode':
88
- return undefined
94
+ return parseContractCode(data)
89
95
  case 'ttl':
90
- return undefined
96
+ return parseTtl(data)
91
97
  default:
92
98
  throw new TxMetaEffectParserError(`Unknown meta entry type: ${updatedEntryType}`)
93
99
  }
@@ -240,46 +246,51 @@ function parseContractData(value) {
240
246
  const owner = parseStateOwnerDataAddress(data.contract())
241
247
 
242
248
  const valueAttr = data.val()
243
- switch (data.key().switch()?.name) {
244
- case 'scvLedgerKeyContractInstance':
245
- const entry = {
246
- entry: 'contract',
247
- contract: owner
248
- }
249
- const instance = valueAttr.instance()._attributes
250
- const type = instance.executable._switch.name
251
- switch (type) {
252
- case 'contractExecutableStellarAsset':
253
- entry.kind = 'fromAsset'
254
- if (!instance.storage?.length)
255
- return undefined //likely the asset has been created "fromAddress" - no metadata in this case
249
+ const entry = {
250
+ entry: 'contractData',
251
+ owner,
252
+ key: data.key().toXDR('base64'),
253
+ value: valueAttr.toXDR('base64'),
254
+ durability: data.durability().name,
255
+ keyHash: generateContractStateEntryHash(data)
256
+ }
257
+ if (data.key().switch()?.name === 'scvLedgerKeyContractInstance' && entry.durability === 'persistent') {
258
+ entry.durability = 'instance'
259
+ const instance = valueAttr.instance()._attributes
260
+ const type = instance.executable._switch.name
261
+ switch (type) {
262
+ case 'contractExecutableStellarAsset':
263
+ entry.kind = 'fromAsset'
264
+ if (instance.storage?.length) { //if not -- the asset has been created "fromAddress" - no metadata in this case
256
265
  const metaArgs = instance.storage[0]._attributes
257
266
  if (metaArgs.key._value.toString() !== 'METADATA')
258
267
  throw new TxMetaEffectParserError('Unexpected asset initialization metadata')
259
268
  entry.asset = xdrParseAsset(metaArgs.val._value[1]._attributes.val._value.toString())
260
- break
261
- case 'contractExecutableWasm':
262
- entry.kind = 'wasm'
263
- entry.hash = instance.executable.wasmHash().toString('hex')
264
- if (instance.storage?.length) {
265
- entry.storage = instance.storage.map(entry => ({
266
- key: entry.key().toXDR('base64'),
267
- val: entry.val().toXDR('base64')
268
- }))
269
- }
270
- break
271
- default:
272
- throw new TxMetaEffectParserError('Unsupported executable type: ' + type)
273
- }
274
- return entry
269
+ }
270
+ break
271
+ case 'contractExecutableWasm':
272
+ entry.kind = 'wasm'
273
+ entry.wasmHash = instance.executable.wasmHash().toString('hex')
274
+ break
275
+ default:
276
+ throw new TxMetaEffectParserError('Unsupported executable type: ' + type)
277
+ }
278
+ if (instance.storage?.length) {
279
+ entry.storage = instance.storage.map(entry => ({
280
+ key: entry.key().toXDR('base64'),
281
+ val: entry.val().toXDR('base64')
282
+ }))
283
+ }
275
284
  }
285
+ return entry
286
+ }
276
287
 
288
+ function parseTtl(data) {
289
+ const attrs = data._value._attributes
277
290
  return {
278
- entry: 'contractData',
279
- owner,
280
- key: data.key().toXDR('base64'),
281
- value: valueAttr.toXDR('base64'),
282
- durability: data.durability().name
291
+ entry: 'ttl',
292
+ keyHash: attrs.keyHash.toString('hex'),
293
+ ttl: attrs.liveUntilLedgerSeq
283
294
  }
284
295
  }
285
296
 
@@ -289,13 +300,14 @@ function parseStateOwnerDataAddress(contract) {
289
300
  return xdrParseAccountAddress(contract.accountId())
290
301
  }
291
302
 
292
- /*function parseContractCode(value) {
303
+ function parseContractCode(value) {
293
304
  const contract = value.value()
305
+ const hash = contract.hash()
294
306
  return {
295
307
  entry: 'contractCode',
296
- hash: contract.hash().toString('hex'),
297
- code: contract.body().code().toString('base64')
308
+ hash: hash.toString('hex'),
309
+ keyHash: generateContractCodeEntryHash(hash)
298
310
  }
299
- }*/
311
+ }
300
312
 
301
313
  module.exports = {parseLedgerEntryChanges}
@@ -0,0 +1,19 @@
1
+ const {xdr, hash} = require('@stellar/stellar-base')
2
+
3
+ function generateContractStateEntryHash(data) {
4
+ const {contract, durability, key} = data._attributes
5
+ const contractDataKey = new xdr.LedgerKeyContractData({contract, durability, key})
6
+ const ledgerKey = xdr.LedgerKey.contractData(contractDataKey)
7
+ return hash(ledgerKey.toXDR()).toString('hex')
8
+ }
9
+
10
+ function generateContractCodeEntryHash(wasmHash) {
11
+ const contractDataKey = new xdr.LedgerKeyContractCode({hash: wasmHash})
12
+ const ledgerKey = xdr.LedgerKey.contractCode(contractDataKey)
13
+ return hash(ledgerKey.toXDR()).toString('hex')
14
+ }
15
+
16
+ module.exports = {
17
+ generateContractStateEntryHash,
18
+ generateContractCodeEntryHash
19
+ }
@@ -234,7 +234,6 @@ function xdrParseAsset(src) {
234
234
  return `${src.code}-${src.issuer}-${src.type || (src.code.length > 4 ? 2 : 1)}`
235
235
  }
236
236
 
237
-
238
237
  function xdrParseScVal(value, treatBytesAsContractId = false) {
239
238
  if (typeof value === 'string') {
240
239
  value = xdr.ScVal.fromXDR(value, 'base64')