@stellar-expert/tx-meta-effects-parser 5.7.0 → 6.0.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stellar-expert/tx-meta-effects-parser",
3
- "version": "5.7.0",
3
+ "version": "6.0.0",
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
 
@@ -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())
@@ -422,6 +426,8 @@ class EffectsAnalyzer {
422
426
  }
423
427
 
424
428
  processDexOperationEffects() {
429
+ if (!this.result)
430
+ return
425
431
  //process trades first
426
432
  for (const claimedOffer of this.result.claimedOffers) {
427
433
  const trade = {
@@ -763,7 +769,7 @@ class EffectsAnalyzer {
763
769
  const balanceEffects = this.effects.filter(e => (e.type === effectTypes.accountCredited || e.type === effectTypes.accountDebited) && e.source === account && e.asset === effect.owner)
764
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
765
771
  return
766
- 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
767
773
  balanceEffects[0].balance = '0'
768
774
  return
769
775
  }
@@ -780,8 +786,8 @@ class EffectsAnalyzer {
780
786
  processContractChanges({action, before, after}) {
781
787
  if (action !== 'created' && action !== 'updated')
782
788
  throw new UnexpectedTxMetaChangeError({type: 'contract', action})
783
- const {kind, contract} = after
784
- const effect = {
789
+ const {kind, owner: contract, keyHash} = after
790
+ let effect = {
785
791
  type: effectTypes.contractCreated,
786
792
  contract,
787
793
  kind
@@ -791,33 +797,38 @@ class EffectsAnalyzer {
791
797
  effect.asset = after.asset
792
798
  break
793
799
  case 'wasm':
794
- effect.wasmHash = after.hash
800
+ effect.wasmHash = after.wasmHash
795
801
  break
796
802
  default:
797
803
  throw new TxMetaEffectParserError('Unexpected contract type: ' + kind)
798
804
  }
799
805
  if (action === 'created') {
800
- if (this.effects.some(e => e.contract === contract))
801
- 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
+ }
802
809
  } else if (action === 'updated') {
803
810
  effect.type = effectTypes.contractUpdated
804
- effect.prevWasmHash = before.hash
805
- if (before.storage?.length || after.storage?.length) {
806
- this.processInstanceDataChanges(before, after)
811
+ effect.prevWasmHash = before.wasmHash
812
+ if (before.wasmHash === after.wasmHash) {//skip if hash unchanged
813
+ effect = undefined
807
814
  }
808
- if (before.hash === after.hash) //skip if hash unchanged
809
- return
810
815
  }
811
- 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
+ }
812
822
  }
813
823
 
814
824
  processContractStateEntryChanges({action, before, after}) {
815
- const {owner, key, durability} = after || before
825
+ const {owner, key, durability, keyHash} = after || before
816
826
  const effect = {
817
827
  type: '',
818
828
  owner,
829
+ key,
819
830
  durability,
820
- key
831
+ keyHash
821
832
  }
822
833
  switch (action) {
823
834
  case 'created':
@@ -840,9 +851,27 @@ class EffectsAnalyzer {
840
851
  this.processContractBalance(effect)
841
852
  }
842
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
+
843
872
  processInstanceDataChanges(before, after) {
844
- const storageBefore = before.storage || []
845
- const storageAfter = [...(after.storage || [])]
873
+ const storageBefore = before?.storage || []
874
+ const storageAfter = [...(after?.storage || [])]
846
875
  for (const {key, val} of storageBefore) {
847
876
  let newVal
848
877
  for (let i = 0; i < storageAfter.length; i++) {
@@ -856,7 +885,7 @@ class EffectsAnalyzer {
856
885
  if (newVal === undefined) { //removed
857
886
  const effect = {
858
887
  type: effectTypes.contractDataRemoved,
859
- owner: after.contract || before.contract,
888
+ owner: after?.owner || before.owner,
860
889
  key,
861
890
  prevValue: val,
862
891
  durability: 'instance'
@@ -869,7 +898,7 @@ class EffectsAnalyzer {
869
898
 
870
899
  const effect = {
871
900
  type: effectTypes.contractDataUpdated,
872
- owner: after.contract || before.contract,
901
+ owner: after?.owner || before.owner,
873
902
  key,
874
903
  value: newVal,
875
904
  prevValue: val,
@@ -881,7 +910,7 @@ class EffectsAnalyzer {
881
910
  for (const {key, val} of storageAfter) {
882
911
  const effect = {
883
912
  type: effectTypes.contractDataCreated,
884
- owner: after.contract || before.contract,
913
+ owner: after?.owner || before.owner,
885
914
  key,
886
915
  value: val,
887
916
  durability: 'instance'
@@ -890,6 +919,27 @@ class EffectsAnalyzer {
890
919
  }
891
920
  }
892
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
+
893
943
  processChanges() {
894
944
  for (const change of this.changes)
895
945
  switch (change.type) {
@@ -912,10 +962,15 @@ class EffectsAnalyzer {
912
962
  this.processDataEntryChanges(change)
913
963
  break
914
964
  case 'contractData':
915
- //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)
916
971
  break
917
- case 'contract':
918
- this.processContractChanges(change)
972
+ case 'ttl':
973
+ this.processTtlChanges(change)
919
974
  break
920
975
  default:
921
976
  throw new UnexpectedTxMetaChangeError(change)
@@ -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')