@stellar-expert/tx-meta-effects-parser 5.0.0-beta10

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.
@@ -0,0 +1,333 @@
1
+ const {StrKey} = require('@stellar/stellar-base')
2
+ const {
3
+ xdrParseAsset,
4
+ xdrParseAccountAddress,
5
+ xdrParseClaimant,
6
+ xdrParsePrice,
7
+ xdrParseSignerKey,
8
+ xdrParseScVal
9
+ } = require('./tx-xdr-parser-utils')
10
+ const {TxMetaEffectParserError} = require('./errors')
11
+
12
+ /**
13
+ * @typedef {{}} ParsedLedgerEntryMeta
14
+ * @property {'account'|'trustline'|'offer'|'data'|'liquidityPool'|'claimableBalance'|'contractData'|'contractCode'} type - Ledger entry type
15
+ * @property {'created'|'updated'|'removed'} action - Ledger modification action
16
+ * @property {{}} before - Ledger entry state before changes applied
17
+ * @property {{}} after - Ledger entry state after changes application
18
+ */
19
+
20
+ /**
21
+ * @param {LedgerEntryChange[]} ledgerEntryChanges
22
+ * @return {ParsedLedgerEntryMeta[]}
23
+ */
24
+ function parseLedgerEntryChanges(ledgerEntryChanges) {
25
+ const changes = []
26
+ let state
27
+ for (let i = 0; i < ledgerEntryChanges.length; i++) {
28
+ const entry = ledgerEntryChanges[i]
29
+ const actionType = entry.arm()
30
+
31
+ const stateData = parseEntry(entry.value(), actionType)
32
+ if (stateData === undefined)
33
+ continue
34
+ const change = {action: actionType}
35
+ switch (actionType) {
36
+ case 'state':
37
+ state = stateData
38
+ continue
39
+ case 'created':
40
+ change.before = null
41
+ change.after = stateData
42
+ change.type = stateData.entry
43
+ break
44
+ case 'updated':
45
+ change.before = state
46
+ change.after = stateData
47
+ change.type = stateData.entry
48
+ break
49
+ case 'removed':
50
+ if (!state && entry._value._arm === 'ttl')
51
+ continue //skip expiration processing for now
52
+ change.before = state
53
+ change.after = null
54
+ change.type = state.entry
55
+ break
56
+ default:
57
+ throw new TxMetaEffectParserError(`Unknown change entry type: ${actionType}`)
58
+ }
59
+ changes.push(change)
60
+ state = null
61
+ }
62
+ return changes
63
+ }
64
+
65
+ function parseEntry(entry, actionType) {
66
+ if (actionType === 'removed')
67
+ return null //parseEntryData(entry)
68
+ const parsed = parseEntryData(entry.data())
69
+ if (parsed === null)
70
+ return null
71
+ //parsed.modified = entry.lastModifiedLedgerSeq()
72
+ return parseLedgerEntryExt(parsed, entry)
73
+ }
74
+
75
+ function parseEntryData(data) {
76
+ const updatedEntryType = data.arm()
77
+ switch (updatedEntryType) {
78
+ case 'account':
79
+ return parseAccountEntry(data)
80
+ case 'trustline':
81
+ case 'trustLine':
82
+ return parseTrustlineEntry(data)
83
+ case 'offer':
84
+ return parseOfferEntry(data)
85
+ case 'data':
86
+ case 'datum':
87
+ return parseDataEntry(data)
88
+ case 'claimableBalance':
89
+ return parseClaimableBalanceEntry(data)
90
+ case 'liquidityPool':
91
+ return parseLiquidityPoolEntry(data)
92
+ case 'contractData':
93
+ return parseContractData(data)
94
+ case 'contractCode':
95
+ return undefined
96
+ case 'ttl':
97
+ return undefined
98
+ default:
99
+ throw new TxMetaEffectParserError(`Unknown meta entry type: ${updatedEntryType}`)
100
+ }
101
+ }
102
+
103
+ function parseLedgerEntryExt(data, entry) {
104
+ const v1 = entry.ext()?.v1()
105
+ if (v1) {
106
+ const sponsor = v1.sponsoringId()
107
+ if (sponsor) {
108
+ data.sponsor = xdrParseAccountAddress(sponsor)
109
+ }
110
+ }
111
+ return data
112
+ }
113
+
114
+ function parseAccountEntry(value) {
115
+ const accountEntryXdr = value.value()
116
+ const data = {
117
+ entry: 'account',
118
+ address: xdrParseAccountAddress(accountEntryXdr.accountId()),
119
+ sequence: accountEntryXdr.seqNum().toString(),
120
+ balance: accountEntryXdr.balance().toString(),
121
+ homeDomain: accountEntryXdr.homeDomain().toString('UTF8'),
122
+ inflationDest: xdrParseAccountAddress(accountEntryXdr.inflationDest()),
123
+ flags: accountEntryXdr.flags(),
124
+ signers: accountEntryXdr.signers().map(signer => ({
125
+ key: xdrParseSignerKey(signer.key()),
126
+ weight: signer.weight()
127
+ }))
128
+ }
129
+ const thresholds = accountEntryXdr.thresholds()
130
+ data.thresholds = thresholds.slice(1).join()
131
+ data.masterWeight = thresholds[0]
132
+ const extV1 = accountEntryXdr.ext()?.v1()
133
+ if (extV1) {
134
+ const extV2 = extV1.ext()?.v2()
135
+ if (extV2) {
136
+ const sponsoringIDs = extV2.signerSponsoringIDs()
137
+ if (sponsoringIDs.length > 0) {
138
+ for (let i = 0; i < data.signers.length; i++) {
139
+ const sponsor = sponsoringIDs[i]
140
+ if (sponsor) { //attach sponsors directly to the signers
141
+ data.signers[i].sponsor = xdrParseAccountAddress(sponsor)
142
+ }
143
+ }
144
+ }
145
+ }
146
+ }
147
+ //ignored fields: numSubEntries, extV1.liabilities, extV2.numSponsored, extV2.numSponsoring, extV3.seqLedger, extv3.seqTime
148
+ return data
149
+ }
150
+
151
+ function parseTrustlineEntry(value) {
152
+ const trustlineEntryXdr = value.value()
153
+ const trustlineAsset = trustlineEntryXdr.asset()
154
+ const trustlineType = trustlineAsset.switch()
155
+ let asset
156
+ switch (trustlineType.value) {
157
+ case 0:
158
+ case 1:
159
+ case 2:
160
+ asset = xdrParseAsset(trustlineAsset)
161
+ break
162
+ case 3:
163
+ asset = trustlineEntryXdr.asset().liquidityPoolId().toString('hex')
164
+ //data.liquidityPoolUseCount = trustlineEntryXdr.liquidityPoolUseCount()
165
+ break
166
+ default:
167
+ throw new TxMetaEffectParserError(`Unsupported trustline type ` + trustlineType)
168
+ }
169
+ const data = {
170
+ entry: 'trustline',
171
+ account: xdrParseAccountAddress(trustlineEntryXdr.accountId()),
172
+ asset,
173
+ balance: trustlineEntryXdr.balance().toString(),
174
+ limit: trustlineEntryXdr.limit().toString(),
175
+ flags: trustlineEntryXdr.flags()
176
+ }
177
+
178
+ /*
179
+ //ignored
180
+ const extV1 = trustlineEntryXdr.ext()?.v1()
181
+ if (extV1) {
182
+ const liabilities = extV1.liabilities()
183
+ data.buying_liabilities = liabilities.buying().toString()
184
+ data.selling_liabilities = liabilities.selling().toString()
185
+ }*/
186
+
187
+ return data
188
+ }
189
+
190
+ function parseDataEntry(value) {
191
+ const dataEntryXdr = value.value()
192
+ return {
193
+ entry: 'data',
194
+ account: xdrParseAccountAddress(dataEntryXdr.accountId()),
195
+ name: dataEntryXdr.dataName().toString(),
196
+ value: dataEntryXdr.dataValue().toString('base64')
197
+ }
198
+ }
199
+
200
+ function parseLiquidityPoolEntry(value) {
201
+ const liquidityPoolEntryXdr = value.value()
202
+ const body = liquidityPoolEntryXdr.body().value()
203
+ const params = body.params()
204
+ return {
205
+ entry: 'liquidityPool',
206
+ pool: liquidityPoolEntryXdr.liquidityPoolId().toString('hex'),
207
+ asset: [xdrParseAsset(params.assetA()), xdrParseAsset(params.assetB())],
208
+ fee: params.fee(),
209
+ amount: [body.reserveA().toString(), body.reserveB().toString()],
210
+ shares: body.totalPoolShares().toString(),
211
+ accounts: body.poolSharesTrustLineCount().low
212
+ }
213
+ }
214
+
215
+ function parseOfferEntry(value) {
216
+ const offerEntryXdr = value.value()
217
+ const data = {
218
+ entry: 'offer',
219
+ id: offerEntryXdr.offerId().toString(),
220
+ account: xdrParseAccountAddress(offerEntryXdr.sellerId()),
221
+ asset: [xdrParseAsset(offerEntryXdr.selling()), xdrParseAsset(offerEntryXdr.buying())],
222
+ amount: offerEntryXdr.amount().toString(),
223
+ price: xdrParsePrice(offerEntryXdr.price()),
224
+ flags: offerEntryXdr.flags()
225
+ }
226
+ return data
227
+ }
228
+
229
+ function parseClaimableBalanceEntry(value) {
230
+ const claimableBalanceXdr = value.value()
231
+ const data = {
232
+ balanceId: Buffer.from(claimableBalanceXdr.balanceId().value()).toString('hex'),
233
+ entry: 'claimableBalance',
234
+ asset: xdrParseAsset(claimableBalanceXdr.asset()),
235
+ amount: claimableBalanceXdr.amount().toString(),
236
+ claimants: claimableBalanceXdr.claimants().map(claimant => xdrParseClaimant(claimant))
237
+ }
238
+ const extV1 = claimableBalanceXdr.ext()?.v1()
239
+ if (extV1) {
240
+ data.flags = extV1.flags()
241
+ }
242
+ return data
243
+ }
244
+
245
+ function parseContractData(value) {
246
+ const data = value.value()
247
+ const owner = parseStateOwnerDataAddress(data.contract())
248
+
249
+ const valueAttr = data.val()
250
+ switch (data.key().switch()?.name) {
251
+ case 'scvLedgerKeyContractInstance':
252
+ const entry = {
253
+ entry: 'contract',
254
+ contract: owner
255
+ }
256
+ const type = valueAttr.instance().executable().switch().name
257
+ switch (type) {
258
+ case 'contractExecutableStellarAsset':
259
+ entry.type = 'token'
260
+ /**
261
+ * data._attributes.val._value._attributes.storage
262
+ *
263
+ * ScVal: [scvContractInstance]
264
+ * instance
265
+ * executable: [contractExecutableStellarAsset]
266
+ * storage: Array[3]
267
+ * [0]
268
+ * key: [scvSymbol]
269
+ * sym: METADATA
270
+ * val: [scvMap]
271
+ * map: Array[3]
272
+ * [0]
273
+ * key: [scvSymbol]
274
+ * sym: decimal
275
+ * val: [scvU32]
276
+ * u32: 7
277
+ * [1]
278
+ * key: [scvSymbol]
279
+ * sym: name
280
+ * val: [scvString]
281
+ * str: ICGVCWUQXIHO:GBD2ALDOSNTEW2QWQA6RGQXTZVWGFZYTT5DYZDCPPGNOYTXOAQ6RFUAC
282
+ * [2]
283
+ * key: [scvSymbol]
284
+ * sym: symbol
285
+ * val: [scvString]
286
+ * str: ICGVCWUQXIHO
287
+ * [1]
288
+ * key: [scvVec]
289
+ * vec: Array[1]
290
+ * [0]: [scvSymbol]
291
+ * sym: Admin
292
+ * val: [scvAddress]
293
+ * address: [scAddressTypeAccount]
294
+ * accountId: [publicKeyTypeEd25519]
295
+ * ed25519: GBD2ALDOSNTEW2QWQA6RGQXTZVWGFZYTT5DYZDCPPGNOYTXOAQ6RFUAC
296
+ */
297
+ return undefined
298
+ break
299
+ case 'contractExecutableWasm':
300
+ entry.kind = 'wasm'
301
+ entry.hash = valueAttr.instance().executable().wasmHash().toString('hex')
302
+ break
303
+ default:
304
+ throw new TxMetaEffectParserError('Unsupported executable type: ' + type)
305
+ }
306
+ return entry
307
+ }
308
+
309
+ return {
310
+ entry: 'contractData',
311
+ owner,
312
+ key: data.key().toXDR('base64'),
313
+ value: valueAttr.toXDR('base64'),
314
+ durability: data.durability().name
315
+ }
316
+ }
317
+
318
+ function parseStateOwnerDataAddress(contract) {
319
+ if (contract.switch().name === 'scAddressTypeContract')
320
+ return StrKey.encodeContract(contract.contractId())
321
+ return xdrParseAccountAddress(contract.accountId())
322
+ }
323
+
324
+ /*function parseContractCode(value) {
325
+ const contract = value.value()
326
+ return {
327
+ entry: 'contractCode',
328
+ hash: contract.hash().toString('hex'),
329
+ code: contract.body().code().toString('base64')
330
+ }
331
+ }*/
332
+
333
+ module.exports = {parseLedgerEntryChanges}
@@ -0,0 +1,147 @@
1
+ const effectTypes = require('./effect-types')
2
+
3
+ class SignerChangesAnalyzer {
4
+ constructor(before, after) {
5
+ this.before = before
6
+ this.after = after
7
+ this.signers = after ? [...after.signers] : []
8
+ }
9
+
10
+ after
11
+
12
+ before
13
+
14
+ signers
15
+
16
+ effects
17
+
18
+ analyze() {
19
+ //search for changes
20
+ this.effects = []
21
+ this.analyzeMasterWeight()
22
+
23
+ const beforeSigners = this.before ? this.before.signers : []
24
+ const afterSigners = this.after ? this.after.signers : []
25
+ //skip if there is nothing to analyze
26
+ if (!beforeSigners.length && !afterSigners.length)
27
+ return this.effects
28
+
29
+ const processed = new Set()
30
+ //compare existing signers
31
+ for (const bs of beforeSigners) {
32
+ //mark signer key as processed
33
+ processed.add(bs.key)
34
+ //locate corresponding post-change signer
35
+ const as = afterSigners.find(signer => signer.key === bs.key)
36
+ if (!as) { //not found -- singer has been removed
37
+ if (bs.sponsor) {
38
+ this.removeSignerSponsorship(bs)
39
+ }
40
+ this.removeAccountSigner(bs)
41
+ continue
42
+ }
43
+
44
+ //check for weight changes
45
+ if (as.weight !== bs.weight) {
46
+ this.updateAccountSigner(as)
47
+ }
48
+ //signer sponsor changed
49
+ if (as.sponsor !== bs.sponsor) {
50
+ if (!bs.sponsor && as.sponsor) {
51
+ this.createSignerSponsorship(as)
52
+ } else if (bs.sponsor && !as.sponsor) {
53
+ this.removeSignerSponsorship(bs)
54
+ } else {
55
+ this.updateSignerSponsorship(as, bs.sponsor)
56
+ }
57
+ }
58
+ }
59
+ //check for new signers
60
+ for (const as of afterSigners) {
61
+ if (!processed.has(as.key)) {
62
+ this.createAccountSigner(as)
63
+ if (as.sponsor) {
64
+ this.createSignerSponsorship(as)
65
+ }
66
+ }
67
+ }
68
+ return this.effects
69
+ }
70
+
71
+ analyzeMasterWeight() {
72
+ const {before, after} = this
73
+ if (!before || !after || before.masterWeight === after.masterWeight)
74
+ return
75
+ const signer = {key: after.address, weight: after.masterWeight}
76
+ if (after.masterWeight > 0) {
77
+ this.updateAccountSigner(signer)
78
+ } else {
79
+ this.removeAccountSigner(signer)
80
+ }
81
+ if (after.masterWeight !== 1) {
82
+ this.signers.push(signer)
83
+ }
84
+ }
85
+
86
+ createAccountSigner(signer) {
87
+ this.effects.push({
88
+ type: effectTypes.accountSignerCreated,
89
+ signer: signer.key,
90
+ weight: signer.weight,
91
+ signers: this.signers
92
+ })
93
+ }
94
+
95
+ removeAccountSigner(signer) {
96
+ this.effects.push({
97
+ type: effectTypes.accountSignerRemoved,
98
+ signer: signer.key,
99
+ weight: 0,
100
+ signers: this.signers
101
+ })
102
+ }
103
+
104
+ updateAccountSigner(signer) {
105
+ this.effects.push({
106
+ type: effectTypes.accountSignerUpdated,
107
+ signer: signer.key,
108
+ weight: signer.weight,
109
+ signers: this.signers
110
+ })
111
+ }
112
+
113
+ createSignerSponsorship(signer) {
114
+ this.effects.push({
115
+ type: effectTypes.signerSponsorshipCreated,
116
+ account: this.after.address,
117
+ signer: signer.key,
118
+ sponsor: signer.sponsor
119
+ })
120
+ }
121
+
122
+ removeSignerSponsorship(signer) {
123
+ this.effects.push({
124
+ type: effectTypes.signerSponsorshipRemoved,
125
+ account: this.before.address,
126
+ signer: signer.key,
127
+ prevSponsor: signer.sponsor
128
+ })
129
+ }
130
+
131
+ updateSignerSponsorship(signer, prevSponsor) {
132
+ this.effects.push({
133
+ type: effectTypes.signerSponsorshipUpdated,
134
+ account: this.after.address,
135
+ signer: signer.key,
136
+ sponsor: signer.sponsor,
137
+ prevSponsor: prevSponsor
138
+ })
139
+ }
140
+ }
141
+
142
+
143
+ function analyzeSignerChanges(before, after) {
144
+ return new SignerChangesAnalyzer(before, after).analyze()
145
+ }
146
+
147
+ module.exports = {analyzeSignerChanges}