@ledgerhq/coin-xrp 7.23.4 → 7.24.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/CHANGELOG.md +12 -0
- package/lib/api/getBlock.d.ts.map +1 -1
- package/lib/api/getBlock.js +17 -5
- package/lib/api/getBlock.js.map +1 -1
- package/lib/api/listOperations.d.ts.map +1 -1
- package/lib/api/listOperations.js +120 -44
- package/lib/api/listOperations.js.map +1 -1
- package/lib/api/validateIntent.d.ts.map +1 -1
- package/lib/api/validateIntent.js +12 -7
- package/lib/api/validateIntent.js.map +1 -1
- package/lib/network/index.d.ts.map +1 -1
- package/lib/network/index.js +2 -0
- package/lib/network/index.js.map +1 -1
- package/lib/types/model.d.ts +1 -0
- package/lib/types/model.d.ts.map +1 -1
- package/lib/utils/errors.d.ts +3 -0
- package/lib/utils/errors.d.ts.map +1 -1
- package/lib/utils/errors.js +2 -1
- package/lib/utils/errors.js.map +1 -1
- package/lib-es/api/getBlock.d.ts.map +1 -1
- package/lib-es/api/getBlock.js +17 -5
- package/lib-es/api/getBlock.js.map +1 -1
- package/lib-es/api/listOperations.d.ts.map +1 -1
- package/lib-es/api/listOperations.js +120 -44
- package/lib-es/api/listOperations.js.map +1 -1
- package/lib-es/api/validateIntent.d.ts.map +1 -1
- package/lib-es/api/validateIntent.js +14 -9
- package/lib-es/api/validateIntent.js.map +1 -1
- package/lib-es/network/index.d.ts.map +1 -1
- package/lib-es/network/index.js +2 -0
- package/lib-es/network/index.js.map +1 -1
- package/lib-es/types/model.d.ts +1 -0
- package/lib-es/types/model.d.ts.map +1 -1
- package/lib-es/utils/errors.d.ts +3 -0
- package/lib-es/utils/errors.d.ts.map +1 -1
- package/lib-es/utils/errors.js +1 -0
- package/lib-es/utils/errors.js.map +1 -1
- package/package.json +1 -1
- package/src/api/getBlock.test.ts +102 -1
- package/src/api/getBlock.ts +23 -5
- package/src/api/index.integ.test.ts +6 -6
- package/src/api/index.test.ts +21 -5
- package/src/api/listOperations.test.ts +211 -3
- package/src/api/listOperations.ts +153 -49
- package/src/api/validateIntent.test.ts +105 -11
- package/src/api/validateIntent.ts +14 -9
- package/src/network/index.test.ts +32 -0
- package/src/network/index.ts +2 -0
- package/src/types/model.ts +1 -0
- package/src/utils/errors.ts +2 -0
package/src/api/index.test.ts
CHANGED
|
@@ -135,13 +135,14 @@ describe('listOperations', () => {
|
|
|
135
135
|
fee: bigint,
|
|
136
136
|
sender: string,
|
|
137
137
|
transactionType: NonPaymentTransactionType = 'OfferCreate',
|
|
138
|
-
destination?: string
|
|
138
|
+
destination?: string,
|
|
139
|
+
affectedNodes?: unknown[]
|
|
139
140
|
): unknown {
|
|
140
141
|
return {
|
|
141
142
|
ledger_hash: 'HASH_VALUE_BLOCK',
|
|
142
143
|
hash: 'NON_PAYMENT_HASH',
|
|
143
144
|
close_time_iso: '2000-01-01T00:00:01Z',
|
|
144
|
-
meta: { TransactionResult: 'tesSUCCESS' },
|
|
145
|
+
meta: { TransactionResult: 'tesSUCCESS', AffectedNodes: affectedNodes },
|
|
145
146
|
tx_json: {
|
|
146
147
|
TransactionType: transactionType,
|
|
147
148
|
Fee: fee.toString(),
|
|
@@ -198,12 +199,27 @@ describe('listOperations', () => {
|
|
|
198
199
|
expect(results).toHaveLength(0)
|
|
199
200
|
})
|
|
200
201
|
|
|
201
|
-
it('should return non-Payment tx when queried address
|
|
202
|
+
it('should return non-Payment tx when queried address receives XRP via AffectedNodes', async () => {
|
|
202
203
|
const fee = BigInt(10)
|
|
203
204
|
const address = 'recipient_address'
|
|
204
205
|
const sender = 'sender_address'
|
|
206
|
+
// AccountDelete sends the remaining balance to the Destination.
|
|
207
|
+
// The recipient's AccountRoot is modified in AffectedNodes.
|
|
208
|
+
const affectedNodes = [
|
|
209
|
+
{
|
|
210
|
+
ModifiedNode: {
|
|
211
|
+
LedgerEntryType: 'AccountRoot',
|
|
212
|
+
LedgerIndex: 'ABC123',
|
|
213
|
+
FinalFields: { Account: address, Balance: '50000000' },
|
|
214
|
+
PreviousFields: { Balance: '10000000' },
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
]
|
|
205
218
|
mockGetTransactions.mockResolvedValue(
|
|
206
|
-
mockNetworkTxs(
|
|
219
|
+
mockNetworkTxs(
|
|
220
|
+
[givenNonPaymentTx(fee, sender, 'AccountDelete', address, affectedNodes)],
|
|
221
|
+
undefined
|
|
222
|
+
)
|
|
207
223
|
)
|
|
208
224
|
|
|
209
225
|
const { items: results } = await api.listOperations(address, { minHeight: 0, order: 'asc' })
|
|
@@ -212,7 +228,7 @@ describe('listOperations', () => {
|
|
|
212
228
|
expect(results[0]).toMatchObject<Partial<Operation>>({
|
|
213
229
|
id: 'NON_PAYMENT_HASH',
|
|
214
230
|
type: 'IN',
|
|
215
|
-
value: BigInt(
|
|
231
|
+
value: BigInt(40000000),
|
|
216
232
|
senders: [sender],
|
|
217
233
|
recipients: [address],
|
|
218
234
|
tx: expect.objectContaining({
|
|
@@ -227,17 +227,225 @@ describe('listOperations', () => {
|
|
|
227
227
|
})
|
|
228
228
|
})
|
|
229
229
|
|
|
230
|
-
it('should exclude non-Payment txs
|
|
231
|
-
// Given — offerCreateTx has Account: "sender",
|
|
230
|
+
it('should exclude non-Payment txs that do not affect the queried address', async () => {
|
|
231
|
+
// Given — offerCreateTx has Account: "sender", no AffectedNodes for "other_address"
|
|
232
232
|
mockNetworkGetTransactions.mockResolvedValue(mockNetworkTxs([offerCreateTx]))
|
|
233
233
|
|
|
234
234
|
// When
|
|
235
235
|
const [results] = await listOperations('other_address', { minHeight: 0, order: 'asc' })
|
|
236
236
|
|
|
237
|
-
// Then —
|
|
237
|
+
// Then — not fee payer, no AffectedNodes balance change → nothing returned
|
|
238
238
|
expect(results).toHaveLength(0)
|
|
239
239
|
})
|
|
240
240
|
|
|
241
|
+
it('should emit an IN operation when a DEX trade fills an offer and the queried address receives XRP', async () => {
|
|
242
|
+
// Given — Account A creates an OfferCreate that fills an offer by "receiver".
|
|
243
|
+
// "receiver" gets 5000000 drops of XRP through the DEX match.
|
|
244
|
+
const dexFillTx = {
|
|
245
|
+
ledger_hash: 'HASH_VALUE_BLOCK',
|
|
246
|
+
hash: 'DEX_FILL_HASH',
|
|
247
|
+
validated: true,
|
|
248
|
+
close_time_iso: '2000-01-01T00:00:01Z',
|
|
249
|
+
meta: {
|
|
250
|
+
TransactionResult: 'tesSUCCESS',
|
|
251
|
+
AffectedNodes: [
|
|
252
|
+
{
|
|
253
|
+
ModifiedNode: {
|
|
254
|
+
LedgerEntryType: 'AccountRoot',
|
|
255
|
+
LedgerIndex: 'ABC123',
|
|
256
|
+
FinalFields: { Account: 'receiver', Balance: '15000000' },
|
|
257
|
+
PreviousFields: { Balance: '10000000' },
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
ModifiedNode: {
|
|
262
|
+
LedgerEntryType: 'AccountRoot',
|
|
263
|
+
LedgerIndex: 'DEF456',
|
|
264
|
+
FinalFields: { Account: 'trader', Balance: '4999988' },
|
|
265
|
+
PreviousFields: { Balance: '10000000' },
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
tx_json: {
|
|
271
|
+
TransactionType: 'OfferCreate',
|
|
272
|
+
Fee: '12',
|
|
273
|
+
ledger_index: 2,
|
|
274
|
+
date: 1000,
|
|
275
|
+
Account: 'trader',
|
|
276
|
+
Sequence: 3,
|
|
277
|
+
SigningPubKey: 'DEADBEEF',
|
|
278
|
+
},
|
|
279
|
+
}
|
|
280
|
+
mockNetworkGetTransactions.mockResolvedValue(mockNetworkTxs([dexFillTx]))
|
|
281
|
+
|
|
282
|
+
// When — query as "receiver" (the address whose offer got filled)
|
|
283
|
+
const [results] = await listOperations('receiver', { minHeight: 0, order: 'asc' })
|
|
284
|
+
|
|
285
|
+
// Then — receiver gets an IN operation for 5000000 drops
|
|
286
|
+
expect(results).toHaveLength(1)
|
|
287
|
+
expect(results[0]).toMatchObject({
|
|
288
|
+
id: 'DEX_FILL_HASH',
|
|
289
|
+
type: 'IN',
|
|
290
|
+
value: BigInt(5000000),
|
|
291
|
+
asset: { type: 'native' },
|
|
292
|
+
tx: expect.objectContaining({
|
|
293
|
+
hash: 'DEX_FILL_HASH',
|
|
294
|
+
fees: BigInt(12),
|
|
295
|
+
feesPayer: 'trader',
|
|
296
|
+
failed: false,
|
|
297
|
+
}),
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('should emit an OUT operation when sender spends XRP through a DEX trade beyond fees', async () => {
|
|
302
|
+
// Given — sender's OfferCreate spends 5000000 drops XRP + 12 drops fee through the DEX
|
|
303
|
+
const dexSpendTx = {
|
|
304
|
+
ledger_hash: 'HASH_VALUE_BLOCK',
|
|
305
|
+
hash: 'DEX_SPEND_HASH',
|
|
306
|
+
validated: true,
|
|
307
|
+
close_time_iso: '2000-01-01T00:00:01Z',
|
|
308
|
+
meta: {
|
|
309
|
+
TransactionResult: 'tesSUCCESS',
|
|
310
|
+
AffectedNodes: [
|
|
311
|
+
{
|
|
312
|
+
ModifiedNode: {
|
|
313
|
+
LedgerEntryType: 'AccountRoot',
|
|
314
|
+
LedgerIndex: 'ABC123',
|
|
315
|
+
FinalFields: { Account: 'sender', Balance: '4999988' },
|
|
316
|
+
PreviousFields: { Balance: '10000000' },
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
},
|
|
321
|
+
tx_json: {
|
|
322
|
+
TransactionType: 'OfferCreate',
|
|
323
|
+
Fee: '12',
|
|
324
|
+
ledger_index: 2,
|
|
325
|
+
date: 1000,
|
|
326
|
+
Account: 'sender',
|
|
327
|
+
Sequence: 4,
|
|
328
|
+
SigningPubKey: 'DEADBEEF',
|
|
329
|
+
},
|
|
330
|
+
}
|
|
331
|
+
mockNetworkGetTransactions.mockResolvedValue(mockNetworkTxs([dexSpendTx]))
|
|
332
|
+
|
|
333
|
+
// When
|
|
334
|
+
const [results] = await listOperations('sender', { minHeight: 0, order: 'asc' })
|
|
335
|
+
|
|
336
|
+
// Then — sender gets an OUT operation for 5000000 drops (fee excluded)
|
|
337
|
+
expect(results).toHaveLength(1)
|
|
338
|
+
expect(results[0]).toMatchObject({
|
|
339
|
+
id: 'DEX_SPEND_HASH',
|
|
340
|
+
type: 'OUT',
|
|
341
|
+
// diff = 4999988 - 10000000 = -5000012, adjustedDiff = -5000012 + 12 = -5000000
|
|
342
|
+
value: BigInt(5000000),
|
|
343
|
+
tx: expect.objectContaining({
|
|
344
|
+
fees: BigInt(12),
|
|
345
|
+
feesPayer: 'sender',
|
|
346
|
+
}),
|
|
347
|
+
})
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('should emit an IN operation when a non-Payment tx creates a new account (CreatedNode)', async () => {
|
|
351
|
+
// Given — a non-Payment tx (e.g. EscrowFinish) creates a new account with an initial balance
|
|
352
|
+
const escrowFinishTx = {
|
|
353
|
+
ledger_hash: 'HASH_VALUE_BLOCK',
|
|
354
|
+
hash: 'ESCROW_HASH',
|
|
355
|
+
validated: true,
|
|
356
|
+
close_time_iso: '2000-01-01T00:00:01Z',
|
|
357
|
+
meta: {
|
|
358
|
+
TransactionResult: 'tesSUCCESS',
|
|
359
|
+
AffectedNodes: [
|
|
360
|
+
{
|
|
361
|
+
CreatedNode: {
|
|
362
|
+
LedgerEntryType: 'AccountRoot',
|
|
363
|
+
LedgerIndex: 'ABC123',
|
|
364
|
+
NewFields: { Account: 'new_account', Balance: '50000000' },
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
},
|
|
369
|
+
tx_json: {
|
|
370
|
+
TransactionType: 'EscrowFinish',
|
|
371
|
+
Fee: '12',
|
|
372
|
+
ledger_index: 2,
|
|
373
|
+
date: 1000,
|
|
374
|
+
Account: 'escrow_sender',
|
|
375
|
+
Destination: 'new_account',
|
|
376
|
+
Sequence: 5,
|
|
377
|
+
SigningPubKey: 'DEADBEEF',
|
|
378
|
+
},
|
|
379
|
+
}
|
|
380
|
+
mockNetworkGetTransactions.mockResolvedValue(mockNetworkTxs([escrowFinishTx]))
|
|
381
|
+
|
|
382
|
+
// When — query as the newly created account
|
|
383
|
+
const [results] = await listOperations('new_account', { minHeight: 0, order: 'asc' })
|
|
384
|
+
|
|
385
|
+
// Then — new account gets an IN operation for its initial balance
|
|
386
|
+
expect(results).toHaveLength(1)
|
|
387
|
+
expect(results[0]).toMatchObject({
|
|
388
|
+
id: 'ESCROW_HASH',
|
|
389
|
+
type: 'IN',
|
|
390
|
+
value: BigInt(50000000),
|
|
391
|
+
senders: ['escrow_sender'],
|
|
392
|
+
recipients: ['new_account'],
|
|
393
|
+
})
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it('should emit an OUT operation when AccountDelete sender account is deleted (DeletedNode)', async () => {
|
|
397
|
+
// Given — sender deletes their account, remaining balance goes to destination.
|
|
398
|
+
// The sender's AccountRoot appears as a DeletedNode with FinalFields.Balance.
|
|
399
|
+
const accountDeleteTx = {
|
|
400
|
+
ledger_hash: 'HASH_VALUE_BLOCK',
|
|
401
|
+
hash: 'ACCT_DELETE_HASH',
|
|
402
|
+
validated: true,
|
|
403
|
+
close_time_iso: '2000-01-01T00:00:01Z',
|
|
404
|
+
meta: {
|
|
405
|
+
TransactionResult: 'tesSUCCESS',
|
|
406
|
+
AffectedNodes: [
|
|
407
|
+
{
|
|
408
|
+
DeletedNode: {
|
|
409
|
+
LedgerEntryType: 'AccountRoot',
|
|
410
|
+
LedgerIndex: 'ABC123',
|
|
411
|
+
FinalFields: { Account: 'deleted_sender', Balance: '30000000' },
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
},
|
|
416
|
+
tx_json: {
|
|
417
|
+
TransactionType: 'AccountDelete',
|
|
418
|
+
Fee: '2000000',
|
|
419
|
+
ledger_index: 2,
|
|
420
|
+
date: 1000,
|
|
421
|
+
Account: 'deleted_sender',
|
|
422
|
+
Destination: 'receiver',
|
|
423
|
+
Sequence: 10,
|
|
424
|
+
SigningPubKey: 'DEADBEEF',
|
|
425
|
+
},
|
|
426
|
+
}
|
|
427
|
+
mockNetworkGetTransactions.mockResolvedValue(mockNetworkTxs([accountDeleteTx]))
|
|
428
|
+
|
|
429
|
+
// When — query as the deleted sender
|
|
430
|
+
const [results] = await listOperations('deleted_sender', { minHeight: 0, order: 'asc' })
|
|
431
|
+
|
|
432
|
+
// Then — sender gets an OUT operation for the remaining balance sent to destination.
|
|
433
|
+
// FinalFields.Balance = 30000000 (post-fee). diff = -(30000000 + 2000000) = -32000000.
|
|
434
|
+
// adjustedDiff = -32000000 + 2000000 = -30000000 → OUT with value 30000000.
|
|
435
|
+
expect(results).toHaveLength(1)
|
|
436
|
+
expect(results[0]).toMatchObject({
|
|
437
|
+
id: 'ACCT_DELETE_HASH',
|
|
438
|
+
type: 'OUT',
|
|
439
|
+
value: BigInt(30000000),
|
|
440
|
+
senders: ['deleted_sender'],
|
|
441
|
+
recipients: ['receiver'],
|
|
442
|
+
tx: expect.objectContaining({
|
|
443
|
+
fees: BigInt(2000000),
|
|
444
|
+
feesPayer: 'deleted_sender',
|
|
445
|
+
}),
|
|
446
|
+
})
|
|
447
|
+
})
|
|
448
|
+
|
|
241
449
|
it('should return both Payment ops and FEES ops when txs contain both types', async () => {
|
|
242
450
|
// Given — paymentTx (Payment from "sender") + offerCreateTx (OfferCreate from "sender")
|
|
243
451
|
mockNetworkGetTransactions.mockResolvedValue(mockNetworkTxs([paymentTx, offerCreateTx]))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: © 2024 LEDGER SAS
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import type { XrplOperation } from '../network/types'
|
|
4
|
+
import type { AffectedNode, XrplOperation } from '../network/types'
|
|
5
5
|
import { Operation } from '@ledgerhq/coin-module-framework/api/types'
|
|
6
6
|
import { getServerInfos, getTransactions, GetTransactionsOptions } from '../network'
|
|
7
7
|
import { ListOperationsOptions, XrpMemo } from '../types'
|
|
@@ -66,27 +66,21 @@ export async function listOperations(
|
|
|
66
66
|
/**
|
|
67
67
|
* Fetches one page of account_tx and splits results into:
|
|
68
68
|
* - Payment transactions (OUT/IN)
|
|
69
|
-
* - Non-Payment transactions
|
|
69
|
+
* - Non-Payment transactions (balance changes extracted from AffectedNodes metadata)
|
|
70
70
|
*
|
|
71
71
|
* tx_type node RPC filter is unreliable (see LIVE-16705), so we filter client-side.
|
|
72
72
|
*/
|
|
73
73
|
async function getAccountTransactions(
|
|
74
74
|
address: string,
|
|
75
75
|
opts: GetTransactionsOptions
|
|
76
|
-
): Promise<[boolean, GetTransactionsOptions, XrplOperation[], XrplOperation[]
|
|
76
|
+
): Promise<[boolean, GetTransactionsOptions, XrplOperation[], XrplOperation[]]> {
|
|
77
77
|
const response = await getTransactions(address, opts)
|
|
78
78
|
const txs = response.transactions
|
|
79
79
|
const responseMarker = response.marker
|
|
80
80
|
|
|
81
81
|
const paymentTxs = txs.filter((tx) => tx.tx_json.TransactionType === 'Payment')
|
|
82
|
-
// Non-Payment transactions
|
|
83
|
-
const
|
|
84
|
-
(tx) => tx.tx_json.TransactionType !== 'Payment' && tx.tx_json.Account === address
|
|
85
|
-
)
|
|
86
|
-
// Non-Payment transactions received by this account: the account received XRP but did not pay fees.
|
|
87
|
-
const receivedTxs = txs.filter(
|
|
88
|
-
(tx) => tx.tx_json.TransactionType !== 'Payment' && tx.tx_json.Destination === address
|
|
89
|
-
)
|
|
82
|
+
// Non-Payment transactions — filtered for balance impact later by convertNonPaymentToCoreOperation
|
|
83
|
+
const nonPaymentTxs = txs.filter((tx) => tx.tx_json.TransactionType !== 'Payment')
|
|
90
84
|
|
|
91
85
|
const shortage = (opts.limit && txs.length < opts.limit) || false
|
|
92
86
|
const { marker: _marker, ...restOpts } = opts
|
|
@@ -96,36 +90,37 @@ export async function listOperations(
|
|
|
96
90
|
nextOpts.marker = responseMarker
|
|
97
91
|
if (nextOpts.limit) nextOpts.limit -= paymentTxs.length
|
|
98
92
|
}
|
|
99
|
-
return [shortage, nextOpts, paymentTxs,
|
|
93
|
+
return [shortage, nextOpts, paymentTxs, nonPaymentTxs]
|
|
100
94
|
}
|
|
101
95
|
|
|
102
|
-
let [txShortage, nextOptions, transactions,
|
|
103
|
-
|
|
96
|
+
let [txShortage, nextOptions, transactions, nonPaymentTxs] = await getAccountTransactions(
|
|
97
|
+
address,
|
|
98
|
+
options
|
|
99
|
+
)
|
|
104
100
|
const isEnough = () => txShortage || (limit && transactions.length >= limit)
|
|
105
101
|
// We need to call the node RPC multiple times to get the desired number of transactions by the limiter.
|
|
106
102
|
while (nextOptions.limit && !isEnough()) {
|
|
107
|
-
const [newTxShortage, newNextOptions, newTransactions,
|
|
103
|
+
const [newTxShortage, newNextOptions, newTransactions, newNonPaymentTxs] =
|
|
108
104
|
await getAccountTransactions(address, nextOptions)
|
|
109
105
|
txShortage = newTxShortage
|
|
110
106
|
nextOptions = newNextOptions
|
|
111
107
|
transactions = transactions.concat(newTransactions)
|
|
112
|
-
|
|
113
|
-
receivedTxs = receivedTxs.concat(newReceivedTxs)
|
|
108
|
+
nonPaymentTxs = nonPaymentTxs.concat(newNonPaymentTxs)
|
|
114
109
|
}
|
|
115
110
|
|
|
116
111
|
// the order is reversed so that the results are always sorted by newest tx first element of the list
|
|
117
112
|
if (order === 'asc') {
|
|
118
113
|
transactions.reverse()
|
|
119
|
-
|
|
120
|
-
receivedTxs.reverse()
|
|
114
|
+
nonPaymentTxs.reverse()
|
|
121
115
|
}
|
|
122
116
|
|
|
123
117
|
// the next index to start the pagination from
|
|
124
118
|
const next = nextOptions.marker ? JSON.stringify(nextOptions.marker) : ''
|
|
125
119
|
const paymentOps = transactions.map(convertToCoreOperation(address))
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
120
|
+
const nonPaymentOps = nonPaymentTxs
|
|
121
|
+
.map(convertNonPaymentToCoreOperation(address))
|
|
122
|
+
.filter((op): op is Operation => op !== null)
|
|
123
|
+
return [[...paymentOps, ...nonPaymentOps], next]
|
|
129
124
|
}
|
|
130
125
|
|
|
131
126
|
const convertToCoreOperation =
|
|
@@ -230,27 +225,106 @@ const convertToCoreOperation =
|
|
|
230
225
|
}
|
|
231
226
|
|
|
232
227
|
/**
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
228
|
+
* Finds the XRP (native) balance change for a specific address in a transaction's AffectedNodes.
|
|
229
|
+
* Returns the balance diff in drops, or null if the address's AccountRoot was not affected.
|
|
230
|
+
*
|
|
231
|
+
* For ModifiedNode: diff = FinalFields.Balance - PreviousFields.Balance (includes fee deduction).
|
|
232
|
+
* For CreatedNode: diff = NewFields.Balance (initial funding amount).
|
|
233
|
+
* For DeletedNode: diff = -(FinalFields.Balance + fee). FinalFields.Balance is the post-fee balance
|
|
234
|
+
* remaining when the account was deleted. We add the fee back so the caller's fee adjustment
|
|
235
|
+
* (adjustedDiff = diff + fee) yields the correct transfer amount.
|
|
236
236
|
*/
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
237
|
+
function findXrpBalanceDiff(
|
|
238
|
+
address: string,
|
|
239
|
+
affectedNodes: AffectedNode[],
|
|
240
|
+
fee: bigint
|
|
241
|
+
): bigint | null {
|
|
242
|
+
for (const node of affectedNodes) {
|
|
243
|
+
if ('ModifiedNode' in node) {
|
|
244
|
+
const data = node.ModifiedNode
|
|
245
|
+
if (
|
|
246
|
+
data.LedgerEntryType === 'AccountRoot' &&
|
|
247
|
+
data.FinalFields?.Account === address &&
|
|
248
|
+
typeof data.FinalFields?.Balance === 'string' &&
|
|
249
|
+
typeof data.PreviousFields?.Balance === 'string'
|
|
250
|
+
) {
|
|
251
|
+
return (
|
|
252
|
+
BigInt(data.FinalFields.Balance as string) - BigInt(data.PreviousFields.Balance as string)
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
} else if ('CreatedNode' in node) {
|
|
256
|
+
const data = node.CreatedNode
|
|
257
|
+
if (
|
|
258
|
+
data.LedgerEntryType === 'AccountRoot' &&
|
|
259
|
+
data.NewFields?.Account === address &&
|
|
260
|
+
typeof data.NewFields?.Balance === 'string'
|
|
261
|
+
) {
|
|
262
|
+
return BigInt(data.NewFields.Balance as string)
|
|
263
|
+
}
|
|
264
|
+
} else if ('DeletedNode' in node) {
|
|
265
|
+
const data = node.DeletedNode
|
|
266
|
+
// AccountDelete: the sender's AccountRoot is deleted. FinalFields.Balance is the post-fee balance
|
|
267
|
+
// (the fee was already deducted before deletion). We return -(Balance + fee) so that the caller's
|
|
268
|
+
// fee adjustment (diff + fee) yields the correct transfer amount (= FinalFields.Balance).
|
|
269
|
+
if (
|
|
270
|
+
data.LedgerEntryType === 'AccountRoot' &&
|
|
271
|
+
data.FinalFields?.Account === address &&
|
|
272
|
+
typeof data.FinalFields?.Balance === 'string'
|
|
273
|
+
) {
|
|
274
|
+
const balance = BigInt(data.FinalFields.Balance as string)
|
|
275
|
+
// Include the fee in the diff so the caller's adjustedDiff = (-balance - fee) + fee = -balance
|
|
276
|
+
return -(balance + fee)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return null
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Converts a non-Payment transaction into an Operation by inspecting AffectedNodes metadata
|
|
285
|
+
* to determine the actual XRP balance change for the queried address.
|
|
286
|
+
*
|
|
287
|
+
* This handles DEX trades (OfferCreate), TrustSet, AccountSet, etc. where the queried address
|
|
288
|
+
* may gain or lose XRP through offer matching — not just pay fees.
|
|
289
|
+
*
|
|
290
|
+
* Returns null if the transaction does not affect the address's XRP balance and the address
|
|
291
|
+
* is not the fee payer.
|
|
292
|
+
*/
|
|
293
|
+
const convertNonPaymentToCoreOperation =
|
|
294
|
+
(address: string) =>
|
|
295
|
+
(operation: XrplOperation): Operation | null => {
|
|
296
|
+
const {
|
|
297
|
+
ledger_hash,
|
|
252
298
|
hash,
|
|
253
|
-
|
|
299
|
+
close_time_iso,
|
|
300
|
+
meta: { AffectedNodes, TransactionResult },
|
|
301
|
+
tx_json: {
|
|
302
|
+
TransactionType,
|
|
303
|
+
Fee,
|
|
304
|
+
date,
|
|
305
|
+
Account,
|
|
306
|
+
Destination,
|
|
307
|
+
ledger_index,
|
|
308
|
+
Sequence,
|
|
309
|
+
SigningPubKey,
|
|
310
|
+
},
|
|
311
|
+
} = operation
|
|
312
|
+
|
|
313
|
+
const fee = BigInt(Fee)
|
|
314
|
+
const toEpochDate = (RIPPLE_EPOCH + date) * 1000
|
|
315
|
+
const failed = TransactionResult !== 'tesSUCCESS'
|
|
316
|
+
const isFeePayer = Account === address
|
|
317
|
+
|
|
318
|
+
// Extract actual XRP balance change from ledger metadata
|
|
319
|
+
const diff = findXrpBalanceDiff(address, AffectedNodes ?? [], fee)
|
|
320
|
+
|
|
321
|
+
// The ledger diff already includes the fee deduction, so add the fee back to isolate the transfer amount.
|
|
322
|
+
// Example: sender trades 5 XRP + 12 drops fee → diff = -5000012, adjustedDiff = -5000012 + 12 = -5000000
|
|
323
|
+
const adjustedDiff = diff !== null && isFeePayer ? diff + fee : diff
|
|
324
|
+
|
|
325
|
+
const baseTx = {
|
|
326
|
+
hash,
|
|
327
|
+
fees: fee,
|
|
254
328
|
feesPayer: Account,
|
|
255
329
|
date: new Date(toEpochDate),
|
|
256
330
|
block: {
|
|
@@ -259,15 +333,45 @@ const convertFeeToCoreOperation = (operation: XrplOperation): Operation => {
|
|
|
259
333
|
height: ledger_index,
|
|
260
334
|
},
|
|
261
335
|
failed,
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
senders: [Account],
|
|
266
|
-
recipients: [],
|
|
267
|
-
details: {
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const details = {
|
|
268
339
|
xrpTxType: TransactionType,
|
|
269
340
|
sequence: Sequence,
|
|
270
341
|
signingPubKey: SigningPubKey,
|
|
271
|
-
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Non-zero transfer amount: emit IN or OUT operation
|
|
345
|
+
if (adjustedDiff !== null && adjustedDiff !== BigInt(0)) {
|
|
346
|
+
const type = adjustedDiff > BigInt(0) ? 'IN' : 'OUT'
|
|
347
|
+
const value = adjustedDiff > BigInt(0) ? adjustedDiff : -adjustedDiff
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
id: hash,
|
|
351
|
+
asset: { type: 'native' },
|
|
352
|
+
tx: baseTx,
|
|
353
|
+
type,
|
|
354
|
+
value,
|
|
355
|
+
senders: [Account],
|
|
356
|
+
recipients: Destination ? [Destination] : [],
|
|
357
|
+
details,
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// No transfer beyond fees — only emit FEES for the fee payer
|
|
362
|
+
if (isFeePayer) {
|
|
363
|
+
return {
|
|
364
|
+
id: hash,
|
|
365
|
+
asset: { type: 'native' },
|
|
366
|
+
tx: baseTx,
|
|
367
|
+
type: 'FEES',
|
|
368
|
+
value: BigInt(0),
|
|
369
|
+
senders: [Account],
|
|
370
|
+
recipients: [],
|
|
371
|
+
details,
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Not fee payer, no XRP balance change — skip
|
|
376
|
+
return null
|
|
272
377
|
}
|
|
273
|
-
}
|