@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/lib/api/getBlock.d.ts.map +1 -1
  3. package/lib/api/getBlock.js +17 -5
  4. package/lib/api/getBlock.js.map +1 -1
  5. package/lib/api/listOperations.d.ts.map +1 -1
  6. package/lib/api/listOperations.js +120 -44
  7. package/lib/api/listOperations.js.map +1 -1
  8. package/lib/api/validateIntent.d.ts.map +1 -1
  9. package/lib/api/validateIntent.js +12 -7
  10. package/lib/api/validateIntent.js.map +1 -1
  11. package/lib/network/index.d.ts.map +1 -1
  12. package/lib/network/index.js +2 -0
  13. package/lib/network/index.js.map +1 -1
  14. package/lib/types/model.d.ts +1 -0
  15. package/lib/types/model.d.ts.map +1 -1
  16. package/lib/utils/errors.d.ts +3 -0
  17. package/lib/utils/errors.d.ts.map +1 -1
  18. package/lib/utils/errors.js +2 -1
  19. package/lib/utils/errors.js.map +1 -1
  20. package/lib-es/api/getBlock.d.ts.map +1 -1
  21. package/lib-es/api/getBlock.js +17 -5
  22. package/lib-es/api/getBlock.js.map +1 -1
  23. package/lib-es/api/listOperations.d.ts.map +1 -1
  24. package/lib-es/api/listOperations.js +120 -44
  25. package/lib-es/api/listOperations.js.map +1 -1
  26. package/lib-es/api/validateIntent.d.ts.map +1 -1
  27. package/lib-es/api/validateIntent.js +14 -9
  28. package/lib-es/api/validateIntent.js.map +1 -1
  29. package/lib-es/network/index.d.ts.map +1 -1
  30. package/lib-es/network/index.js +2 -0
  31. package/lib-es/network/index.js.map +1 -1
  32. package/lib-es/types/model.d.ts +1 -0
  33. package/lib-es/types/model.d.ts.map +1 -1
  34. package/lib-es/utils/errors.d.ts +3 -0
  35. package/lib-es/utils/errors.d.ts.map +1 -1
  36. package/lib-es/utils/errors.js +1 -0
  37. package/lib-es/utils/errors.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/api/getBlock.test.ts +102 -1
  40. package/src/api/getBlock.ts +23 -5
  41. package/src/api/index.integ.test.ts +6 -6
  42. package/src/api/index.test.ts +21 -5
  43. package/src/api/listOperations.test.ts +211 -3
  44. package/src/api/listOperations.ts +153 -49
  45. package/src/api/validateIntent.test.ts +105 -11
  46. package/src/api/validateIntent.ts +14 -9
  47. package/src/network/index.test.ts +32 -0
  48. package/src/network/index.ts +2 -0
  49. package/src/types/model.ts +1 -0
  50. package/src/utils/errors.ts +2 -0
@@ -3,7 +3,7 @@
3
3
 
4
4
  import { TransactionIntent } from '@ledgerhq/coin-module-framework/api/types'
5
5
  import { XrpMapMemo } from '../types'
6
- import { XrpInvalidMemoError } from '../utils/errors'
6
+ import { XrpDestinationTagRequired, XrpInvalidMemoError } from '../utils/errors'
7
7
  import * as logicValidateMemo from '../utils/validateMemo'
8
8
  import { validateIntent } from './validateIntent'
9
9
 
@@ -12,6 +12,17 @@ const mockGetBalance = jest.fn()
12
12
  const mockGetServerInfos = jest.fn()
13
13
 
14
14
  const RECIPIENT_NEW = 'rDKsbvy9uaNpPtvVFraJyNGfjvTw8xivgK'
15
+ const RECIPIENT_TAG_REQUIRED = 'rwnYLUsoBQX3ECa1A5bSKLdbPoHKnqf63J'
16
+
17
+ const mockGetAccountInfo = jest.fn((addr: string) =>
18
+ Promise.resolve({
19
+ isNewAccount: addr === RECIPIENT_NEW,
20
+ balance: '0',
21
+ ownerCount: 0,
22
+ sequence: 0,
23
+ requireDestinationTag: addr === RECIPIENT_TAG_REQUIRED,
24
+ })
25
+ )
15
26
 
16
27
  jest.mock('./getBalance', () => ({
17
28
  getBalance: () => mockGetBalance(),
@@ -19,16 +30,7 @@ jest.mock('./getBalance', () => ({
19
30
 
20
31
  jest.mock('../network', () => ({
21
32
  getServerInfos: () => mockGetServerInfos(),
22
- }))
23
-
24
- jest.mock('../utils', () => ({
25
- ...jest.requireActual('../utils'),
26
- recipientIsNew: jest.fn((addr: string) => {
27
- if (addr === RECIPIENT_NEW) {
28
- return Promise.resolve(true)
29
- }
30
- return Promise.resolve(false)
31
- }),
33
+ getAccountInfo: (addr: string) => mockGetAccountInfo(addr),
32
34
  }))
33
35
 
34
36
  jest.mock('../utils/validateMemo')
@@ -418,4 +420,96 @@ describe('validateIntent', () => {
418
420
 
419
421
  expect(spiedValidateMemo).toHaveBeenCalledWith(memos.get('destinationTag'))
420
422
  })
423
+
424
+ it('errors when destinationTag is not a single string value', async () => {
425
+ mockGetServerInfos.mockResolvedValue({
426
+ info: {
427
+ validated_ledger: {
428
+ reserve_base_xrp: reserveBase / 1_000_000n,
429
+ },
430
+ },
431
+ })
432
+
433
+ const memos = new Map<string, string | string[]>()
434
+ memos.set('destinationTag', ['12345', '67890'])
435
+
436
+ const status = await validateIntent(
437
+ {
438
+ intentType: 'transaction',
439
+ sender: SENDER,
440
+ asset: { unit: { code: 'XRP', magnitude: 6 } },
441
+ amount: 1_000_000n,
442
+ recipient: RECIPIENT,
443
+ memo: {
444
+ type: '',
445
+ memos,
446
+ },
447
+ } as TransactionIntent<XrpMapMemo>,
448
+ [{ value: 50_000_000n, asset: { type: 'native' }, locked: 0n }],
449
+ { value: 10_000n } // fees
450
+ )
451
+
452
+ expect(status.errors.transaction).toBeInstanceOf(XrpInvalidMemoError)
453
+ })
454
+
455
+ it('errors when recipient requires a destination tag and none is provided', async () => {
456
+ mockGetServerInfos.mockResolvedValue({
457
+ info: {
458
+ validated_ledger: {
459
+ reserve_base_xrp: reserveBase / 1_000_000n,
460
+ },
461
+ },
462
+ })
463
+
464
+ const status = await validateIntent(
465
+ {
466
+ intentType: 'transaction',
467
+ sender: SENDER,
468
+ asset: { unit: { code: 'XRP', magnitude: 6 } },
469
+ amount: 20_000_000n,
470
+ recipient: RECIPIENT_TAG_REQUIRED,
471
+ memo: {
472
+ type: '',
473
+ memos: new Map<string, string | string[]>(),
474
+ },
475
+ } as TransactionIntent<XrpMapMemo>,
476
+ [{ value: 50_000_000n, asset: { type: 'native' }, locked: 0n }],
477
+ { value: 10_000n } // fees
478
+ )
479
+
480
+ expect(status.errors.transaction).toBeInstanceOf(XrpDestinationTagRequired)
481
+ })
482
+
483
+ it('does not error when recipient requires a destination tag and one is provided', async () => {
484
+ mockGetServerInfos.mockResolvedValue({
485
+ info: {
486
+ validated_ledger: {
487
+ reserve_base_xrp: reserveBase / 1_000_000n,
488
+ },
489
+ },
490
+ })
491
+
492
+ spiedValidateMemo.mockReturnValueOnce(true)
493
+
494
+ const memos = new Map<string, string | string[]>()
495
+ memos.set('destinationTag', '12345')
496
+
497
+ const status = await validateIntent(
498
+ {
499
+ intentType: 'transaction',
500
+ sender: SENDER,
501
+ asset: { unit: { code: 'XRP', magnitude: 6 } },
502
+ amount: 20_000_000n,
503
+ recipient: RECIPIENT_TAG_REQUIRED,
504
+ memo: {
505
+ type: '',
506
+ memos,
507
+ },
508
+ } as TransactionIntent<XrpMapMemo>,
509
+ [{ value: 50_000_000n, asset: { type: 'native' }, locked: 0n }],
510
+ { value: 10_000n } // fees
511
+ )
512
+
513
+ expect(status.errors.transaction).not.toBeDefined()
514
+ })
421
515
  })
@@ -19,11 +19,10 @@ import {
19
19
  RecipientRequired,
20
20
  } from '@ledgerhq/errors'
21
21
  import { isValidClassicAddress } from 'ripple-address-codec'
22
- import { getServerInfos } from '../network'
22
+ import { getAccountInfo, getServerInfos } from '../network'
23
23
  import { XrpMapMemo } from '../types'
24
- import { recipientIsNew } from '../utils'
25
24
  import { parseAPIValue } from '../utils/common'
26
- import { XrpInvalidMemoError } from '../utils/errors'
25
+ import { XrpDestinationTagRequired, XrpInvalidMemoError } from '../utils/errors'
27
26
  import { validateMemo } from '../utils/validateMemo'
28
27
 
29
28
  export const validateIntent = async (
@@ -41,6 +40,11 @@ export const validateIntent = async (
41
40
  const totalSpent = transactionIntent.amount + estimatedFees
42
41
  const amount = transactionIntent.amount
43
42
 
43
+ const recipientInfo =
44
+ transactionIntent.recipient && isValidClassicAddress(transactionIntent.recipient)
45
+ ? await getAccountInfo(transactionIntent.recipient)
46
+ : undefined
47
+
44
48
  if (amount > 0 && estimatedFees * 10n > amount) {
45
49
  warnings.feeTooHigh = new FeeTooHigh()
46
50
  }
@@ -63,8 +67,7 @@ export const validateIntent = async (
63
67
  : 'Unknown unit',
64
68
  })
65
69
  } else if (
66
- transactionIntent.recipient &&
67
- (await recipientIsNew(transactionIntent.recipient)) &&
70
+ recipientInfo?.isNewAccount &&
68
71
  transactionIntent.amount < BigInt(reserveBaseXRP.toString())
69
72
  ) {
70
73
  errors.amount = new NotEnoughBalanceBecauseDestinationNotCreated('', {
@@ -92,11 +95,13 @@ export const validateIntent = async (
92
95
  errors.amount = new AmountRequired()
93
96
  }
94
97
 
95
- const destinationTag = transactionIntent.memo?.memos
96
- ? transactionIntent.memo.memos.get('destinationTag')
97
- : undefined
98
- if (destinationTag && typeof destinationTag === 'string' && !validateMemo(destinationTag)) {
98
+ const destinationTag = transactionIntent.memo?.memos?.get('destinationTag')
99
+ if (destinationTag !== undefined && typeof destinationTag !== 'string') {
100
+ errors.transaction = new XrpInvalidMemoError()
101
+ } else if (destinationTag && !validateMemo(destinationTag)) {
99
102
  errors.transaction = new XrpInvalidMemoError()
103
+ } else if (!errors.recipient && !destinationTag && recipientInfo?.requireDestinationTag) {
104
+ errors.transaction = new XrpDestinationTagRequired()
100
105
  }
101
106
 
102
107
  return {
@@ -49,6 +49,7 @@ describe('getAccountInfo', () => {
49
49
  balance: '0',
50
50
  ownerCount: 0,
51
51
  sequence: 0,
52
+ requireDestinationTag: false,
52
53
  })
53
54
  })
54
55
 
@@ -86,9 +87,40 @@ describe('getAccountInfo', () => {
86
87
  balance: '999441667919804',
87
88
  ownerCount: 0,
88
89
  sequence: 153743,
90
+ requireDestinationTag: false,
89
91
  })
90
92
  })
91
93
 
94
+ it('returns requireDestinationTag true when the account_flags flag is set', async () => {
95
+ const address = 'rwnYLUsoBQX3ECa1A5bSKLdbPoHKnqf63J'
96
+ ;(network as jest.Mock).mockResolvedValue({
97
+ data: {
98
+ result: {
99
+ account_data: {
100
+ Account: address,
101
+ Balance: '100000000',
102
+ Flags: 131072,
103
+ LedgerEntryType: 'AccountRoot',
104
+ OwnerCount: 0,
105
+ Sequence: 100,
106
+ index: 'BC0DAE09C0BFBC4A49AA94B849266588BFD6E1F554B184B5788AC55D6E07EB95',
107
+ },
108
+ account_flags: {
109
+ requireDestinationTag: true,
110
+ },
111
+ ledger_hash: '93E952B2770233B0ABFBFBBFBC3E2E2159DCABD07FEB5F4C49174027935D9FBB',
112
+ ledger_index: 1908009,
113
+ status: 'success',
114
+ validated: true,
115
+ },
116
+ },
117
+ })
118
+
119
+ const result = await getAccountInfo(address)
120
+
121
+ expect(result.requireDestinationTag).toBe(true)
122
+ })
123
+
92
124
  it('throws an error when backend returns any other error', async () => {
93
125
  // Given
94
126
  const invalidAddress = 'rNCgVpHinUDjXP2vHDFDMjm7ssBwpveHyaa'
@@ -61,6 +61,7 @@ export const getAccountInfo = async (
61
61
  balance: '0',
62
62
  ownerCount: 0,
63
63
  sequence: 0,
64
+ requireDestinationTag: false,
64
65
  }
65
66
  } else {
66
67
  return {
@@ -68,6 +69,7 @@ export const getAccountInfo = async (
68
69
  balance: result.account_data.Balance,
69
70
  ownerCount: result.account_data.OwnerCount,
70
71
  sequence: result.account_data.Sequence,
72
+ requireDestinationTag: result.account_flags?.requireDestinationTag ?? false,
71
73
  }
72
74
  }
73
75
  }
@@ -19,6 +19,7 @@ export type AccountInfo = {
19
19
  balance: string
20
20
  ownerCount: number
21
21
  sequence: number
22
+ requireDestinationTag: boolean
22
23
  }
23
24
 
24
25
  export type XrpMemo = {
@@ -4,3 +4,5 @@
4
4
  import { createCustomErrorClass } from '@ledgerhq/errors'
5
5
 
6
6
  export const XrpInvalidMemoError = createCustomErrorClass('XrpInvalidMemo')
7
+
8
+ export const XrpDestinationTagRequired = createCustomErrorClass('XrpDestinationTagRequired')