@ledgerhq/coin-canton 0.9.0-nightly.3 → 0.9.0-nightly.5

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 (115) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.unimportedrc.json +2 -1
  3. package/CHANGELOG.md +29 -0
  4. package/lib/bridge/estimateMaxSpendable.d.ts +2 -2
  5. package/lib/bridge/estimateMaxSpendable.d.ts.map +1 -1
  6. package/lib/bridge/estimateMaxSpendable.js +2 -3
  7. package/lib/bridge/estimateMaxSpendable.js.map +1 -1
  8. package/lib/bridge/getTransactionStatus.d.ts +5 -3
  9. package/lib/bridge/getTransactionStatus.d.ts.map +1 -1
  10. package/lib/bridge/getTransactionStatus.js +26 -5
  11. package/lib/bridge/getTransactionStatus.js.map +1 -1
  12. package/lib/bridge/index.d.ts +2 -2
  13. package/lib/bridge/index.d.ts.map +1 -1
  14. package/lib/bridge/index.js +3 -3
  15. package/lib/bridge/index.js.map +1 -1
  16. package/lib/bridge/prepareTransaction.js +1 -1
  17. package/lib/bridge/prepareTransaction.js.map +1 -1
  18. package/lib/bridge/serialization.d.ts +4 -0
  19. package/lib/bridge/serialization.d.ts.map +1 -0
  20. package/lib/bridge/serialization.js +36 -0
  21. package/lib/bridge/serialization.js.map +1 -0
  22. package/lib/bridge/sync.d.ts.map +1 -1
  23. package/lib/bridge/sync.js +13 -5
  24. package/lib/bridge/sync.js.map +1 -1
  25. package/lib/bridge/transaction.js +1 -1
  26. package/lib/bridge/transaction.js.map +1 -1
  27. package/lib/common-logic/account/getBalance.d.ts +5 -1
  28. package/lib/common-logic/account/getBalance.d.ts.map +1 -1
  29. package/lib/common-logic/account/getBalance.js +2 -0
  30. package/lib/common-logic/account/getBalance.js.map +1 -1
  31. package/lib/common-logic/transaction/craftTransaction.js +1 -1
  32. package/lib/common-logic/transaction/craftTransaction.js.map +1 -1
  33. package/lib/network/gateway.d.ts +1 -0
  34. package/lib/network/gateway.d.ts.map +1 -1
  35. package/lib/network/gateway.js.map +1 -1
  36. package/lib/test/fixtures.d.ts +5 -0
  37. package/lib/test/fixtures.d.ts.map +1 -0
  38. package/lib/test/fixtures.js +57 -0
  39. package/lib/test/fixtures.js.map +1 -0
  40. package/lib/types/bridge.d.ts +14 -2
  41. package/lib/types/bridge.d.ts.map +1 -1
  42. package/lib/types/errors.d.ts +6 -0
  43. package/lib/types/errors.d.ts.map +1 -1
  44. package/lib/types/errors.js +3 -1
  45. package/lib/types/errors.js.map +1 -1
  46. package/lib/types/index.d.ts +1 -0
  47. package/lib/types/index.d.ts.map +1 -1
  48. package/lib/types/index.js +1 -0
  49. package/lib/types/index.js.map +1 -1
  50. package/lib-es/bridge/estimateMaxSpendable.d.ts +2 -2
  51. package/lib-es/bridge/estimateMaxSpendable.d.ts.map +1 -1
  52. package/lib-es/bridge/estimateMaxSpendable.js +2 -3
  53. package/lib-es/bridge/estimateMaxSpendable.js.map +1 -1
  54. package/lib-es/bridge/getTransactionStatus.d.ts +5 -3
  55. package/lib-es/bridge/getTransactionStatus.d.ts.map +1 -1
  56. package/lib-es/bridge/getTransactionStatus.js +26 -5
  57. package/lib-es/bridge/getTransactionStatus.js.map +1 -1
  58. package/lib-es/bridge/index.d.ts +2 -2
  59. package/lib-es/bridge/index.d.ts.map +1 -1
  60. package/lib-es/bridge/index.js +3 -3
  61. package/lib-es/bridge/index.js.map +1 -1
  62. package/lib-es/bridge/prepareTransaction.js +1 -1
  63. package/lib-es/bridge/prepareTransaction.js.map +1 -1
  64. package/lib-es/bridge/serialization.d.ts +4 -0
  65. package/lib-es/bridge/serialization.d.ts.map +1 -0
  66. package/lib-es/bridge/serialization.js +32 -0
  67. package/lib-es/bridge/serialization.js.map +1 -0
  68. package/lib-es/bridge/sync.d.ts.map +1 -1
  69. package/lib-es/bridge/sync.js +13 -5
  70. package/lib-es/bridge/sync.js.map +1 -1
  71. package/lib-es/bridge/transaction.js +1 -1
  72. package/lib-es/bridge/transaction.js.map +1 -1
  73. package/lib-es/common-logic/account/getBalance.d.ts +5 -1
  74. package/lib-es/common-logic/account/getBalance.d.ts.map +1 -1
  75. package/lib-es/common-logic/account/getBalance.js +2 -0
  76. package/lib-es/common-logic/account/getBalance.js.map +1 -1
  77. package/lib-es/common-logic/transaction/craftTransaction.js +1 -1
  78. package/lib-es/common-logic/transaction/craftTransaction.js.map +1 -1
  79. package/lib-es/network/gateway.d.ts +1 -0
  80. package/lib-es/network/gateway.d.ts.map +1 -1
  81. package/lib-es/network/gateway.js.map +1 -1
  82. package/lib-es/test/fixtures.d.ts +5 -0
  83. package/lib-es/test/fixtures.d.ts.map +1 -0
  84. package/lib-es/test/fixtures.js +49 -0
  85. package/lib-es/test/fixtures.js.map +1 -0
  86. package/lib-es/types/bridge.d.ts +14 -2
  87. package/lib-es/types/bridge.d.ts.map +1 -1
  88. package/lib-es/types/errors.d.ts +6 -0
  89. package/lib-es/types/errors.d.ts.map +1 -1
  90. package/lib-es/types/errors.js +2 -0
  91. package/lib-es/types/errors.js.map +1 -1
  92. package/lib-es/types/index.d.ts +1 -0
  93. package/lib-es/types/index.d.ts.map +1 -1
  94. package/lib-es/types/index.js +1 -0
  95. package/lib-es/types/index.js.map +1 -1
  96. package/package.json +7 -7
  97. package/src/bridge/estimateMaxSpendable.ts +6 -8
  98. package/src/bridge/getTransactionStatus.test.ts +84 -146
  99. package/src/bridge/getTransactionStatus.ts +42 -7
  100. package/src/bridge/index.ts +6 -5
  101. package/src/bridge/onboard.integ.test.ts +8 -31
  102. package/src/bridge/prepareTransaction.ts +1 -1
  103. package/src/bridge/serialization.ts +44 -0
  104. package/src/bridge/signOperation.test.ts +3 -9
  105. package/src/bridge/sync.test.ts +4 -0
  106. package/src/bridge/sync.ts +17 -6
  107. package/src/bridge/transaction.ts +1 -1
  108. package/src/common-logic/account/getBalance.ts +12 -2
  109. package/src/common-logic/account/getBalance.unit.test.ts +7 -1
  110. package/src/common-logic/transaction/craftTransaction.ts +1 -1
  111. package/src/network/gateway.ts +1 -0
  112. package/src/test/fixtures.ts +53 -0
  113. package/src/types/bridge.ts +15 -2
  114. package/src/types/errors.ts +3 -0
  115. package/src/types/index.ts +1 -0
@@ -1,6 +1,3 @@
1
- import BigNumber from "bignumber.js";
2
- import { Account } from "@ledgerhq/types-live";
3
- import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
4
1
  import {
5
2
  AmountRequired,
6
3
  FeeNotLoaded,
@@ -12,76 +9,37 @@ import {
12
9
  NotEnoughSpendableBalance,
13
10
  RecipientRequired,
14
11
  } from "@ledgerhq/errors";
15
- import { getTransactionStatus } from "./getTransactionStatus";
16
- import { Transaction } from "../types";
12
+ import BigNumber from "bignumber.js";
17
13
  import coinConfig from "../config";
14
+ import { createMockAccount } from "../test/fixtures";
15
+ import { CantonAccount, TooManyUtxosCritical, TooManyUtxosWarning, Transaction } from "../types";
16
+ import {
17
+ getTransactionStatus,
18
+ TO_MANY_UTXOS_CRITICAL_COUNT,
19
+ TO_MANY_UTXOS_WARNING_COUNT,
20
+ } from "./getTransactionStatus";
18
21
 
19
- // Mock the coin config
20
- jest.mock("../config", () => ({
21
- getCoinConfig: jest.fn(),
22
- }));
23
-
22
+ jest.mock("../config", () => ({ getCoinConfig: jest.fn() }));
24
23
  const mockCoinConfig = jest.mocked(coinConfig);
25
24
 
26
25
  describe("getTransactionStatus", () => {
27
- const mockCurrency: CryptoCurrency = {
28
- id: "canton_network",
29
- name: "Canton Network",
30
- family: "canton",
31
- units: [
32
- {
33
- name: "Canton",
34
- code: "CANTON",
35
- magnitude: 8,
26
+ const mockAccount: CantonAccount = {
27
+ ...createMockAccount({
28
+ balance: new BigNumber(1000),
29
+ spendableBalance: new BigNumber(1000),
30
+ freshAddress: "test::33333333333333333333333333333333333333333333333333333333333333333333",
31
+ }),
32
+ cantonResources: {
33
+ instrumentUtxoCounts: {
34
+ Amulet: 5,
36
35
  },
37
- ],
38
- ticker: "CANTON",
39
- scheme: "canton",
40
- color: "#000000",
41
- type: "CryptoCurrency",
42
- managerAppName: "Canton",
43
- coinType: 0,
44
- disableCountervalue: false,
45
- delisted: false,
46
- keywords: ["canton"],
47
- explorerViews: [],
48
- terminated: {
49
- link: "",
50
36
  },
51
37
  };
52
38
 
53
- const mockAccount: Account = {
54
- id: "test-account-id",
55
- seedIdentifier: "test-seed-identifier",
56
- currency: mockCurrency,
57
- balance: new BigNumber(1000), // 1000 units
58
- spendableBalance: new BigNumber(1000),
59
- freshAddress: "test::33333333333333333333333333333333333333333333333333333333333333333333",
60
- freshAddressPath: "44'/60'/0'/0/0",
61
- index: 0,
62
- derivationMode: "canton",
63
- used: true,
64
- operations: [],
65
- pendingOperations: [],
66
- lastSyncDate: new Date(),
67
- creationDate: new Date(),
68
- operationsCount: 0,
69
- blockHeight: 100,
70
- balanceHistoryCache: {
71
- HOUR: { latestDate: null, balances: [] },
72
- DAY: { latestDate: null, balances: [] },
73
- WEEK: { latestDate: null, balances: [] },
74
- },
75
- swapHistory: [],
76
- nfts: [],
77
- subAccounts: [],
78
- type: "Account",
79
- };
80
-
81
39
  beforeEach(() => {
82
40
  jest.clearAllMocks();
83
41
  mockCoinConfig.getCoinConfig.mockReturnValue({
84
- minReserve: 100, // 100 units minimum reserve
42
+ minReserve: 100,
85
43
  networkType: "mainnet",
86
44
  status: { type: "active" },
87
45
  nativeInstrumentId: "Amulet",
@@ -122,9 +80,9 @@ describe("getTransactionStatus", () => {
122
80
  it("should add FeeTooHigh warning when fee is more than 10 times the amount", async () => {
123
81
  const transaction: Transaction = {
124
82
  family: "canton",
125
- amount: new BigNumber(100), // Use larger amount to avoid balance issues
83
+ amount: new BigNumber(100),
126
84
  recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
127
- fee: new BigNumber(1500), // 15x the amount
85
+ fee: new BigNumber(1500),
128
86
  tokenId: "",
129
87
  };
130
88
 
@@ -139,7 +97,7 @@ describe("getTransactionStatus", () => {
139
97
  family: "canton",
140
98
  amount: new BigNumber(100),
141
99
  recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
142
- fee: new BigNumber(10), // 0.1x the amount
100
+ fee: new BigNumber(10),
143
101
  tokenId: "",
144
102
  };
145
103
 
@@ -154,7 +112,9 @@ describe("getTransactionStatus", () => {
154
112
  it("should return NotEnoughSpendableBalance error when total spent exceeds balance minus reserve", async () => {
155
113
  const transaction: Transaction = {
156
114
  family: "canton",
157
- amount: new BigNumber(950), // 950 + 10 fee = 960, but balance is 1000 and reserve is 100
115
+ amount: mockAccount.balance
116
+ .minus(new BigNumber(mockCoinConfig.getCoinConfig().minReserve))
117
+ .plus(1),
158
118
  recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
159
119
  fee: new BigNumber(10),
160
120
  tokenId: "",
@@ -168,7 +128,7 @@ describe("getTransactionStatus", () => {
168
128
  it("should return NotEnoughBalanceBecauseDestinationNotCreated error when amount is below reserve", async () => {
169
129
  const transaction: Transaction = {
170
130
  family: "canton",
171
- amount: new BigNumber(50), // Below reserve amount of 100
131
+ amount: new BigNumber(mockCoinConfig.getCoinConfig().minReserve).minus(1),
172
132
  recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
173
133
  fee: new BigNumber(10),
174
134
  tokenId: "",
@@ -182,7 +142,7 @@ describe("getTransactionStatus", () => {
182
142
  it("should pass balance validation when transaction is within limits", async () => {
183
143
  const transaction: Transaction = {
184
144
  family: "canton",
185
- amount: new BigNumber(800), // 800 + 10 fee = 810, balance is 1000, reserve is 100, so 900 available
145
+ amount: mockAccount.balance.multipliedBy(0.5),
186
146
  recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
187
147
  fee: new BigNumber(10),
188
148
  tokenId: "",
@@ -209,18 +169,18 @@ describe("getTransactionStatus", () => {
209
169
  expect(result.errors.recipient).toBeInstanceOf(RecipientRequired);
210
170
  });
211
171
 
212
- it("should return InvalidAddressBecauseDestinationIsAlsoSource error when sending to self", async () => {
172
+ it("should not return error when sending to self", async () => {
213
173
  const transaction: Transaction = {
214
174
  family: "canton",
215
175
  amount: new BigNumber(100),
216
- recipient: "test::33333333333333333333333333333333333333333333333333333333333333333333", // Same as account.freshAddress
176
+ recipient: mockAccount.freshAddress,
217
177
  fee: new BigNumber(10),
218
178
  tokenId: "",
219
179
  };
220
180
 
221
181
  const result = await getTransactionStatus(mockAccount, transaction);
222
182
 
223
- expect(result.errors.recipient).toBeInstanceOf(InvalidAddressBecauseDestinationIsAlsoSource);
183
+ expect(result.errors.recipient).toBeUndefined();
224
184
  });
225
185
 
226
186
  it("should return InvalidAddress error when recipient is invalid", async () => {
@@ -254,16 +214,8 @@ describe("getTransactionStatus", () => {
254
214
 
255
215
  describe("amount validation", () => {
256
216
  it("should return AmountRequired error when amount is zero", async () => {
257
- // Create a scenario where there are no other amount errors
258
- // Use a high balance and amount above reserve to avoid other amount errors
259
- const accountWithHighBalance = {
260
- ...mockAccount,
261
- balance: new BigNumber(10000), // High balance to avoid balance errors
262
- };
263
-
264
- // Set a high reserve to avoid the NotEnoughBalanceBecauseDestinationNotCreated error
265
217
  mockCoinConfig.getCoinConfig.mockReturnValue({
266
- minReserve: 0, // Set reserve to 0 to avoid reserve-related errors
218
+ minReserve: 0,
267
219
  networkType: "mainnet",
268
220
  status: { type: "active" },
269
221
  nativeInstrumentId: "Amulet",
@@ -277,7 +229,7 @@ describe("getTransactionStatus", () => {
277
229
  tokenId: "",
278
230
  };
279
231
 
280
- const result = await getTransactionStatus(accountWithHighBalance, transaction);
232
+ const result = await getTransactionStatus(mockAccount, transaction);
281
233
 
282
234
  expect(result.errors.amount).toBeInstanceOf(AmountRequired);
283
235
  });
@@ -330,11 +282,15 @@ describe("getTransactionStatus", () => {
330
282
  });
331
283
  });
332
284
 
333
- describe("edge cases", () => {
334
- it("should handle account with zero balance", async () => {
335
- const accountWithZeroBalance = {
285
+ describe("UTXO count validation", () => {
286
+ it("should show critical warning when UTXO count exceeds TO_MANY_UTXOS_CRITICAL_COUNT", async () => {
287
+ const accountWithTooManyUtxos = {
336
288
  ...mockAccount,
337
- balance: new BigNumber(0),
289
+ cantonResources: {
290
+ instrumentUtxoCounts: {
291
+ Amulet: TO_MANY_UTXOS_CRITICAL_COUNT + 1,
292
+ },
293
+ },
338
294
  };
339
295
 
340
296
  const transaction: Transaction = {
@@ -342,18 +298,23 @@ describe("getTransactionStatus", () => {
342
298
  amount: new BigNumber(50),
343
299
  recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
344
300
  fee: new BigNumber(10),
345
- tokenId: "",
301
+ tokenId: "Amulet",
346
302
  };
347
303
 
348
- const result = await getTransactionStatus(accountWithZeroBalance, transaction);
304
+ const result = await getTransactionStatus(accountWithTooManyUtxos, transaction);
349
305
 
350
- expect(result.errors.amount).toBeInstanceOf(NotEnoughSpendableBalance);
306
+ expect(result.warnings.tooManyUtxos).toBeDefined();
307
+ expect(result.warnings.tooManyUtxos).toBeInstanceOf(TooManyUtxosCritical);
351
308
  });
352
309
 
353
- it("should handle account with balance exactly equal to reserve", async () => {
354
- const accountWithReserveBalance = {
310
+ it("should show warning when UTXO count exceeds TO_MANY_UTXOS_WARNING_COUNT", async () => {
311
+ const accountWithManyUtxos = {
355
312
  ...mockAccount,
356
- balance: new BigNumber(100), // Exactly equal to reserve
313
+ cantonResources: {
314
+ instrumentUtxoCounts: {
315
+ Amulet: TO_MANY_UTXOS_WARNING_COUNT + 1,
316
+ },
317
+ },
357
318
  };
358
319
 
359
320
  const transaction: Transaction = {
@@ -361,86 +322,63 @@ describe("getTransactionStatus", () => {
361
322
  amount: new BigNumber(50),
362
323
  recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
363
324
  fee: new BigNumber(10),
364
- tokenId: "",
325
+ tokenId: "Amulet",
365
326
  };
366
327
 
367
- const result = await getTransactionStatus(accountWithReserveBalance, transaction);
328
+ const result = await getTransactionStatus(accountWithManyUtxos, transaction);
368
329
 
369
- expect(result.errors.amount).toBeInstanceOf(NotEnoughSpendableBalance);
330
+ expect(result.warnings.tooManyUtxos).toBeDefined();
331
+ expect(result.warnings.tooManyUtxos).toBeInstanceOf(TooManyUtxosWarning);
332
+ expect(result.warnings.tooManyUtxos?.message).toContain(
333
+ "families.canton.tooManyUtxos.warning",
334
+ );
370
335
  });
371
336
 
372
- it("should handle zero reserve amount", async () => {
373
- mockCoinConfig.getCoinConfig.mockReturnValue({
374
- minReserve: 0,
375
- networkType: "mainnet",
376
- status: { type: "active" },
377
- nativeInstrumentId: "Amulet",
378
- });
379
-
380
- const transaction: Transaction = {
381
- family: "canton",
382
- amount: new BigNumber(50),
383
- recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
384
- fee: new BigNumber(10),
385
- tokenId: "",
337
+ it("should not show warning or error when UTXO count is less than TO_MANY_UTXOS_WARNING_COUNT", async () => {
338
+ const accountWithFewUtxos = {
339
+ ...mockAccount,
340
+ cantonResources: {
341
+ instrumentUtxoCounts: {
342
+ Amulet: TO_MANY_UTXOS_WARNING_COUNT - 1,
343
+ },
344
+ },
386
345
  };
387
346
 
388
- const result = await getTransactionStatus(mockAccount, transaction);
389
-
390
- expect(result.errors.amount).toBeUndefined();
391
- });
392
-
393
- it("should handle undefined reserve amount", async () => {
394
- mockCoinConfig.getCoinConfig.mockReturnValue({
395
- networkType: "mainnet",
396
- status: { type: "active" },
397
- nativeInstrumentId: "Amulet",
398
- });
399
-
400
347
  const transaction: Transaction = {
401
348
  family: "canton",
402
349
  amount: new BigNumber(50),
403
350
  recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
404
351
  fee: new BigNumber(10),
405
- tokenId: "",
352
+ tokenId: "Amulet", // Use the same tokenId as in cantonResources
406
353
  };
407
354
 
408
- const result = await getTransactionStatus(mockAccount, transaction);
355
+ const result = await getTransactionStatus(accountWithFewUtxos, transaction);
409
356
 
410
- expect(result.errors.amount).toBeUndefined();
357
+ expect(result.warnings.tooManyUtxos).toBeUndefined();
411
358
  });
412
- });
413
359
 
414
- describe("multiple validation errors", () => {
415
- it("should return multiple errors when multiple validations fail", async () => {
416
- const transaction: Transaction = {
417
- family: "canton",
418
- amount: new BigNumber(0), // AmountRequired
419
- recipient: "", // RecipientRequired
420
- fee: null, // FeeNotLoaded
421
- tokenId: "",
360
+ it("should not show warning or error for abandon seed address transactions", async () => {
361
+ const accountWithManyUtxos = {
362
+ ...mockAccount,
363
+ cantonResources: {
364
+ instrumentUtxoCounts: {
365
+ Amulet: 25,
366
+ },
367
+ },
422
368
  };
423
369
 
424
- const result = await getTransactionStatus(mockAccount, transaction);
425
-
426
- expect(result.errors.amount).toBeInstanceOf(AmountRequired);
427
- expect(result.errors.recipient).toBeInstanceOf(RecipientRequired);
428
- expect(result.errors.fee).toBeInstanceOf(FeeNotLoaded);
429
- });
430
-
431
- it("should return both errors and warnings", async () => {
432
370
  const transaction: Transaction = {
433
371
  family: "canton",
434
- amount: new BigNumber(5), // Small amount
435
- recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
436
- fee: new BigNumber(100), // High fee relative to amount
437
- tokenId: "",
372
+ amount: new BigNumber(50),
373
+ recipient: "abandon::ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
374
+ fee: new BigNumber(10),
375
+ tokenId: "Amulet",
438
376
  };
439
377
 
440
- const result = await getTransactionStatus(mockAccount, transaction);
378
+ const result = await getTransactionStatus(accountWithManyUtxos, transaction);
441
379
 
442
- expect(result.warnings.feeTooHigh).toBeInstanceOf(FeeTooHigh);
443
- expect(result.errors.amount).toBeInstanceOf(NotEnoughBalanceBecauseDestinationNotCreated);
380
+ expect(result.warnings.tooManyUtxos).toBeUndefined();
381
+ expect(result.errors.utxoCount).toBeUndefined();
444
382
  });
445
383
  });
446
384
  });
@@ -4,21 +4,30 @@ import {
4
4
  FeeRequired,
5
5
  FeeTooHigh,
6
6
  InvalidAddress,
7
- InvalidAddressBecauseDestinationIsAlsoSource,
8
7
  NotEnoughBalanceBecauseDestinationNotCreated,
9
8
  NotEnoughSpendableBalance,
10
9
  RecipientRequired,
11
10
  } from "@ledgerhq/errors";
12
11
  import BigNumber from "bignumber.js";
13
- import { Account, AccountBridge } from "@ledgerhq/types-live";
12
+ import { AccountBridge } from "@ledgerhq/types-live";
14
13
  import { formatCurrencyUnit } from "@ledgerhq/coin-framework/currencies/index";
15
- import { Transaction, TransactionStatus } from "../types";
14
+ import {
15
+ Transaction,
16
+ TransactionStatus,
17
+ CantonAccount,
18
+ TooManyUtxosCritical,
19
+ TooManyUtxosWarning,
20
+ } from "../types";
16
21
  import { isRecipientValid } from "../common-logic/utils";
17
22
  import coinConfig from "../config";
23
+ import { getAbandonSeedAddress } from "@ledgerhq/cryptoassets/abandonseed";
24
+
25
+ export const TO_MANY_UTXOS_CRITICAL_COUNT = 24;
26
+ export const TO_MANY_UTXOS_WARNING_COUNT = 10;
18
27
 
19
28
  export const getTransactionStatus: AccountBridge<
20
29
  Transaction,
21
- Account,
30
+ CantonAccount,
22
31
  TransactionStatus
23
32
  >["getTransactionStatus"] = async (account, transaction) => {
24
33
  const errors: Record<string, Error> = {};
@@ -63,9 +72,6 @@ export const getTransactionStatus: AccountBridge<
63
72
 
64
73
  if (!transaction.recipient) {
65
74
  errors.recipient = new RecipientRequired("");
66
- } else if (account.freshAddress === transaction.recipient) {
67
- // we want to prevent user from sending to themselves (even if it's technically feasible)
68
- errors.recipient = new InvalidAddressBecauseDestinationIsAlsoSource();
69
75
  } else if (!isRecipientValid(transaction.recipient)) {
70
76
  // We want to prevent user from sending to an invalid address
71
77
  errors.recipient = new InvalidAddress("", {
@@ -78,6 +84,8 @@ export const getTransactionStatus: AccountBridge<
78
84
  errors.amount = new AmountRequired();
79
85
  }
80
86
 
87
+ validateUtxoCount(account, transaction, warnings);
88
+
81
89
  return {
82
90
  errors,
83
91
  warnings,
@@ -86,3 +94,30 @@ export const getTransactionStatus: AccountBridge<
86
94
  totalSpent,
87
95
  };
88
96
  };
97
+
98
+ function validateUtxoCount(
99
+ account: CantonAccount,
100
+ transaction: Transaction,
101
+ warnings: Record<string, Error>,
102
+ ): void {
103
+ const abandonSeedAddress = getAbandonSeedAddress(account.currency.id);
104
+ const isAbandonSeedAddress = transaction.recipient?.includes(abandonSeedAddress);
105
+
106
+ // UTXO count validation - only validate if recipient is valid and not equal to sender
107
+ // Skip validation for abandon seed addresses
108
+ if (
109
+ account?.cantonResources?.instrumentUtxoCounts &&
110
+ transaction.recipient &&
111
+ isRecipientValid(transaction.recipient) &&
112
+ account.xpub !== transaction.recipient &&
113
+ !isAbandonSeedAddress
114
+ ) {
115
+ const { instrumentUtxoCounts } = account.cantonResources;
116
+ const instrumentUtxoCount = instrumentUtxoCounts[transaction.tokenId] || 0;
117
+ if (instrumentUtxoCount > TO_MANY_UTXOS_CRITICAL_COUNT) {
118
+ warnings.tooManyUtxos = new TooManyUtxosCritical();
119
+ } else if (instrumentUtxoCount > TO_MANY_UTXOS_WARNING_COUNT) {
120
+ warnings.tooManyUtxos = new TooManyUtxosWarning("families.canton.tooManyUtxos.warning");
121
+ }
122
+ }
123
+ }
@@ -10,7 +10,7 @@ import { SignerContext } from "@ledgerhq/coin-framework/signer";
10
10
  import type { AccountBridge } from "@ledgerhq/types-live";
11
11
  import cantonCoinConfig, { type CantonCoinConfig } from "../config";
12
12
  import resolver from "../signer";
13
- import { CantonCurrencyBridge, CantonSigner } from "../types";
13
+ import { CantonCurrencyBridge, CantonSigner, CantonAccount } from "../types";
14
14
  import type { Transaction } from "../types";
15
15
  import { broadcast } from "./broadcast";
16
16
  import { createTransaction } from "./createTransaction";
@@ -21,6 +21,7 @@ import { buildSignOperation } from "./signOperation";
21
21
  import { makeGetAccountShape } from "./sync";
22
22
  import { updateTransaction } from "./updateTransaction";
23
23
  import { buildOnboardAccount, buildAuthorizePreapproval } from "./onboard";
24
+ import { assignToAccountRaw, assignFromAccountRaw } from "./serialization";
24
25
 
25
26
  export function createBridges(
26
27
  signerContext: SignerContext<CantonSigner>,
@@ -49,13 +50,11 @@ export function createBridges(
49
50
 
50
51
  const signOperation = buildSignOperation(signerContext);
51
52
  const sync = makeSync({ getAccountShape: makeGetAccountShape(signerContext) });
52
- // we want one method per file
53
- const accountBridge: AccountBridge<Transaction> = {
53
+
54
+ const accountBridge: AccountBridge<Transaction, CantonAccount> = {
54
55
  broadcast,
55
56
  createTransaction,
56
57
  updateTransaction,
57
- // NOTE: use updateTransaction: defaultUpdateTransaction<Transaction>,
58
- // if you don't need to update the transaction patch object
59
58
  prepareTransaction,
60
59
  getTransactionStatus,
61
60
  estimateMaxSpendable,
@@ -65,6 +64,8 @@ export function createBridges(
65
64
  signRawOperation: () => {
66
65
  throw new Error("signRawOperation is not supported");
67
66
  },
67
+ assignToAccountRaw,
68
+ assignFromAccountRaw,
68
69
  getSerializedAddressParameters,
69
70
  };
70
71
 
@@ -1,45 +1,22 @@
1
- import BigNumber from "bignumber.js";
2
- import { firstValueFrom, toArray } from "rxjs";
3
1
  import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
4
- import { emptyHistoryCache } from "@ledgerhq/coin-framework/account/index";
5
- import { generateMockKeyPair, createMockSigner } from "../test/cantonTestUtils";
2
+ import { firstValueFrom, toArray } from "rxjs";
3
+ import coinConfig from "../config";
4
+ import { createMockSigner, generateMockKeyPair } from "../test/cantonTestUtils";
5
+ import { createMockAccount, createMockCantonCurrency } from "../test/fixtures";
6
6
  import {
7
7
  AuthorizeStatus,
8
- OnboardStatus,
9
8
  CantonAuthorizeProgress,
10
9
  CantonAuthorizeResult,
11
10
  CantonOnboardProgress,
12
11
  CantonOnboardResult,
12
+ OnboardStatus,
13
13
  } from "../types/onboard";
14
- import coinConfig from "../config";
15
- import { buildOnboardAccount, isAccountOnboarded, buildAuthorizePreapproval } from "./onboard";
14
+ import { buildAuthorizePreapproval, buildOnboardAccount, isAccountOnboarded } from "./onboard";
16
15
 
17
16
  describe("onboard (devnet)", () => {
18
17
  const mockDeviceId = "test-device-id";
19
- const mockCurrency = {
20
- id: "canton_network",
21
- } as unknown as CryptoCurrency;
22
- const mockAccount = {
23
- type: "Account" as const,
24
- id: "js:2:canton_network:canton_3f5c9d9a:canton",
25
- seedIdentifier: "canton_3f5c9d9a",
26
- derivationMode: "canton" as const,
27
- index: 0,
28
- freshAddress: "canton_3f5c9d9a",
29
- freshAddressPath: "44'/6767'/0'/0'/0'",
30
- used: false,
31
- balance: BigNumber(10000),
32
- spendableBalance: BigNumber(10000),
33
- creationDate: new Date(),
34
- blockHeight: 1,
35
- currency: mockCurrency,
36
- operationsCount: 0,
37
- operations: [],
38
- pendingOperations: [],
39
- lastSyncDate: new Date(),
40
- balanceHistoryCache: emptyHistoryCache,
41
- swapHistory: [],
42
- };
18
+ const mockCurrency = createMockCantonCurrency();
19
+ const mockAccount = createMockAccount();
43
20
 
44
21
  let onboardedAccount: {
45
22
  keyPair: ReturnType<typeof generateMockKeyPair>;
@@ -11,7 +11,7 @@ export const prepareTransaction: AccountBridge<Transaction>["prepareTransaction"
11
11
  ) => {
12
12
  const amount = transaction.amount || BigNumber(0);
13
13
  const fee = BigNumber(
14
- (await estimateFees(account.currency, BigInt(amount.toString()))).toString(),
14
+ (await estimateFees(account.currency, BigInt(amount.toFixed()))).toString(),
15
15
  );
16
16
 
17
17
  if (!transaction.tokenId) {
@@ -0,0 +1,44 @@
1
+ import type { Account, AccountRaw } from "@ledgerhq/types-live";
2
+ import {
3
+ type CantonAccount,
4
+ type CantonAccountRaw,
5
+ type CantonResources,
6
+ type CantonResourcesRaw,
7
+ } from "../types";
8
+
9
+ function isCantonAccount(account: Account): account is CantonAccount {
10
+ return "cantonResources" in account;
11
+ }
12
+
13
+ function isCantonAccountRaw(accountRaw: AccountRaw): accountRaw is CantonAccountRaw {
14
+ return "cantonResources" in accountRaw;
15
+ }
16
+
17
+ function toResourcesRaw(r: CantonResources): CantonResourcesRaw {
18
+ const { instrumentUtxoCounts } = r;
19
+ return {
20
+ instrumentUtxoCounts,
21
+ };
22
+ }
23
+
24
+ function fromResourcesRaw(r: CantonResourcesRaw): CantonResources {
25
+ return {
26
+ instrumentUtxoCounts: r.instrumentUtxoCounts,
27
+ };
28
+ }
29
+
30
+ export function assignToAccountRaw(account: Account, accountRaw: AccountRaw): void {
31
+ if (isCantonAccount(account) && isCantonAccountRaw(accountRaw)) {
32
+ if (account.cantonResources) {
33
+ accountRaw.cantonResources = toResourcesRaw(account.cantonResources);
34
+ }
35
+ }
36
+ }
37
+
38
+ export function assignFromAccountRaw(accountRaw: AccountRaw, account: Account): void {
39
+ if (isCantonAccountRaw(accountRaw) && isCantonAccount(account)) {
40
+ if (accountRaw.cantonResources) {
41
+ account.cantonResources = fromResourcesRaw(accountRaw.cantonResources);
42
+ }
43
+ }
44
+ }
@@ -9,6 +9,7 @@ import { Transaction } from "../types";
9
9
  import { craftTransaction } from "../common-logic";
10
10
  import prepareTransferMock from "../test/prepare-transfer.json";
11
11
  import { buildSignOperation } from "./signOperation";
12
+ import { createMockAccount } from "../test/fixtures";
12
13
 
13
14
  jest.mock("../common-logic", () => {
14
15
  const actual = jest.requireActual("../common-logic");
@@ -54,25 +55,18 @@ class MockCantonSigner implements CantonSigner {
54
55
  describe("buildSignOperation", () => {
55
56
  const mockDeviceId = "test-device-id";
56
57
  const mockDerivationPath = "44'/6767'/0'/0'/0'";
57
- const mockPartyId = "alice::1220d466a5d96a3509736c821e25fe81fc8a73f226d92e57e94a65170e58b07fc08e";
58
58
 
59
59
  beforeEach(() => {
60
60
  jest.clearAllMocks();
61
61
  mockCraftTransaction.mockReset();
62
62
  });
63
63
 
64
- const mockAccount = {
64
+ const mockAccount = createMockAccount({
65
65
  id: "js:2:canton_network:test-party-id:",
66
66
  freshAddress: "test-address",
67
67
  freshAddressPath: mockDerivationPath,
68
68
  xpub: "test-party-id",
69
- currency: {
70
- id: "canton_network",
71
- },
72
- cantonResources: {
73
- partyId: mockPartyId,
74
- },
75
- } as any;
69
+ });
76
70
 
77
71
  const mockTransaction: Transaction = {
78
72
  family: "canton",
@@ -52,6 +52,7 @@ describe("makeGetAccountShape", () => {
52
52
  mockedCoinConfig.mockReturnValue({
53
53
  nativeInstrumentId: "Native",
54
54
  minReserve: "0",
55
+ useGateway: true,
55
56
  });
56
57
 
57
58
  mockedIsAuthorized.mockResolvedValue(true);
@@ -64,6 +65,7 @@ describe("makeGetAccountShape", () => {
64
65
  instrument_id: "Native",
65
66
  amount: "1000",
66
67
  locked: false,
68
+ utxo_count: 1,
67
69
  },
68
70
  ]);
69
71
  mockedGetOperations.mockResolvedValue({
@@ -113,11 +115,13 @@ describe("makeGetAccountShape", () => {
113
115
  instrument_id: "LockedNative",
114
116
  amount: "1000",
115
117
  locked: true,
118
+ utxo_count: 1,
116
119
  },
117
120
  {
118
121
  instrument_id: "Native",
119
122
  amount: "10",
120
123
  locked: false,
124
+ utxo_count: 1,
121
125
  },
122
126
  ]);
123
127