@ledgerhq/coin-canton 0.9.0-nightly.1 → 0.9.0-nightly.20251030160608

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 (205) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.unimportedrc.json +2 -1
  3. package/CHANGELOG.md +26 -9
  4. package/README.md +75 -0
  5. package/lib/bridge/deviceTransactionConfig.d.ts +1 -1
  6. package/lib/bridge/deviceTransactionConfig.d.ts.map +1 -1
  7. package/lib/bridge/deviceTransactionConfig.js +1 -1
  8. package/lib/bridge/deviceTransactionConfig.js.map +1 -1
  9. package/lib/bridge/estimateMaxSpendable.d.ts +2 -2
  10. package/lib/bridge/estimateMaxSpendable.d.ts.map +1 -1
  11. package/lib/bridge/estimateMaxSpendable.js +2 -3
  12. package/lib/bridge/estimateMaxSpendable.js.map +1 -1
  13. package/lib/bridge/getTransactionStatus.d.ts +5 -3
  14. package/lib/bridge/getTransactionStatus.d.ts.map +1 -1
  15. package/lib/bridge/getTransactionStatus.js +27 -10
  16. package/lib/bridge/getTransactionStatus.js.map +1 -1
  17. package/lib/bridge/index.d.ts +2 -2
  18. package/lib/bridge/index.d.ts.map +1 -1
  19. package/lib/bridge/index.js +3 -3
  20. package/lib/bridge/index.js.map +1 -1
  21. package/lib/bridge/onboard.d.ts.map +1 -1
  22. package/lib/bridge/onboard.js +48 -22
  23. package/lib/bridge/onboard.js.map +1 -1
  24. package/lib/bridge/prepareTransaction.js +1 -1
  25. package/lib/bridge/prepareTransaction.js.map +1 -1
  26. package/lib/bridge/serialization.d.ts +4 -0
  27. package/lib/bridge/serialization.d.ts.map +1 -0
  28. package/lib/bridge/serialization.js +36 -0
  29. package/lib/bridge/serialization.js.map +1 -0
  30. package/lib/bridge/signOperation.d.ts.map +1 -1
  31. package/lib/bridge/signOperation.js +11 -4
  32. package/lib/bridge/signOperation.js.map +1 -1
  33. package/lib/bridge/sync.d.ts.map +1 -1
  34. package/lib/bridge/sync.js +14 -6
  35. package/lib/bridge/sync.js.map +1 -1
  36. package/lib/bridge/transaction.js +1 -1
  37. package/lib/bridge/transaction.js.map +1 -1
  38. package/lib/common-logic/account/getBalance.d.ts +5 -1
  39. package/lib/common-logic/account/getBalance.d.ts.map +1 -1
  40. package/lib/common-logic/account/getBalance.js +2 -0
  41. package/lib/common-logic/account/getBalance.js.map +1 -1
  42. package/lib/common-logic/index.d.ts +1 -0
  43. package/lib/common-logic/index.d.ts.map +1 -1
  44. package/lib/common-logic/index.js +3 -1
  45. package/lib/common-logic/index.js.map +1 -1
  46. package/lib/common-logic/transaction/craftTransaction.js +1 -1
  47. package/lib/common-logic/transaction/craftTransaction.js.map +1 -1
  48. package/lib/common-logic/transaction/estimateFees.js +1 -1
  49. package/lib/common-logic/transaction/estimateFees.js.map +1 -1
  50. package/lib/common-logic/transaction/sign.d.ts +8 -0
  51. package/lib/common-logic/transaction/sign.d.ts.map +1 -0
  52. package/lib/common-logic/transaction/sign.js +45 -0
  53. package/lib/common-logic/transaction/sign.js.map +1 -0
  54. package/lib/common-logic/transaction/split.d.ts +9 -0
  55. package/lib/common-logic/transaction/split.d.ts.map +1 -0
  56. package/lib/common-logic/transaction/split.js +119 -0
  57. package/lib/common-logic/transaction/split.js.map +1 -0
  58. package/lib/common-logic/utils.js +2 -2
  59. package/lib/common-logic/utils.js.map +1 -1
  60. package/lib/network/gateway.d.ts +6 -1
  61. package/lib/network/gateway.d.ts.map +1 -1
  62. package/lib/network/gateway.js +6 -9
  63. package/lib/network/gateway.js.map +1 -1
  64. package/lib/test/cantonTestUtils.d.ts +4 -9
  65. package/lib/test/cantonTestUtils.d.ts.map +1 -1
  66. package/lib/test/cantonTestUtils.js +736 -27
  67. package/lib/test/cantonTestUtils.js.map +1 -1
  68. package/lib/test/fixtures.d.ts +5 -0
  69. package/lib/test/fixtures.d.ts.map +1 -0
  70. package/lib/test/fixtures.js +57 -0
  71. package/lib/test/fixtures.js.map +1 -0
  72. package/lib/types/bridge.d.ts +14 -2
  73. package/lib/types/bridge.d.ts.map +1 -1
  74. package/lib/types/errors.d.ts +6 -0
  75. package/lib/types/errors.d.ts.map +1 -1
  76. package/lib/types/errors.js +3 -1
  77. package/lib/types/errors.js.map +1 -1
  78. package/lib/types/index.d.ts +1 -0
  79. package/lib/types/index.d.ts.map +1 -1
  80. package/lib/types/index.js +1 -0
  81. package/lib/types/index.js.map +1 -1
  82. package/lib/types/signer.d.ts +15 -2
  83. package/lib/types/signer.d.ts.map +1 -1
  84. package/lib/types/transaction-proto.json +1238 -0
  85. package/lib-es/bridge/deviceTransactionConfig.d.ts +1 -1
  86. package/lib-es/bridge/deviceTransactionConfig.d.ts.map +1 -1
  87. package/lib-es/bridge/deviceTransactionConfig.js +1 -1
  88. package/lib-es/bridge/deviceTransactionConfig.js.map +1 -1
  89. package/lib-es/bridge/estimateMaxSpendable.d.ts +2 -2
  90. package/lib-es/bridge/estimateMaxSpendable.d.ts.map +1 -1
  91. package/lib-es/bridge/estimateMaxSpendable.js +2 -3
  92. package/lib-es/bridge/estimateMaxSpendable.js.map +1 -1
  93. package/lib-es/bridge/getTransactionStatus.d.ts +5 -3
  94. package/lib-es/bridge/getTransactionStatus.d.ts.map +1 -1
  95. package/lib-es/bridge/getTransactionStatus.js +27 -10
  96. package/lib-es/bridge/getTransactionStatus.js.map +1 -1
  97. package/lib-es/bridge/index.d.ts +2 -2
  98. package/lib-es/bridge/index.d.ts.map +1 -1
  99. package/lib-es/bridge/index.js +3 -3
  100. package/lib-es/bridge/index.js.map +1 -1
  101. package/lib-es/bridge/onboard.d.ts.map +1 -1
  102. package/lib-es/bridge/onboard.js +49 -23
  103. package/lib-es/bridge/onboard.js.map +1 -1
  104. package/lib-es/bridge/prepareTransaction.js +1 -1
  105. package/lib-es/bridge/prepareTransaction.js.map +1 -1
  106. package/lib-es/bridge/serialization.d.ts +4 -0
  107. package/lib-es/bridge/serialization.d.ts.map +1 -0
  108. package/lib-es/bridge/serialization.js +32 -0
  109. package/lib-es/bridge/serialization.js.map +1 -0
  110. package/lib-es/bridge/signOperation.d.ts.map +1 -1
  111. package/lib-es/bridge/signOperation.js +11 -4
  112. package/lib-es/bridge/signOperation.js.map +1 -1
  113. package/lib-es/bridge/sync.d.ts.map +1 -1
  114. package/lib-es/bridge/sync.js +14 -6
  115. package/lib-es/bridge/sync.js.map +1 -1
  116. package/lib-es/bridge/transaction.js +1 -1
  117. package/lib-es/bridge/transaction.js.map +1 -1
  118. package/lib-es/common-logic/account/getBalance.d.ts +5 -1
  119. package/lib-es/common-logic/account/getBalance.d.ts.map +1 -1
  120. package/lib-es/common-logic/account/getBalance.js +2 -0
  121. package/lib-es/common-logic/account/getBalance.js.map +1 -1
  122. package/lib-es/common-logic/index.d.ts +1 -0
  123. package/lib-es/common-logic/index.d.ts.map +1 -1
  124. package/lib-es/common-logic/index.js +1 -0
  125. package/lib-es/common-logic/index.js.map +1 -1
  126. package/lib-es/common-logic/transaction/craftTransaction.js +1 -1
  127. package/lib-es/common-logic/transaction/craftTransaction.js.map +1 -1
  128. package/lib-es/common-logic/transaction/estimateFees.js +1 -1
  129. package/lib-es/common-logic/transaction/estimateFees.js.map +1 -1
  130. package/lib-es/common-logic/transaction/sign.d.ts +8 -0
  131. package/lib-es/common-logic/transaction/sign.d.ts.map +1 -0
  132. package/lib-es/common-logic/transaction/sign.js +42 -0
  133. package/lib-es/common-logic/transaction/sign.js.map +1 -0
  134. package/lib-es/common-logic/transaction/split.d.ts +9 -0
  135. package/lib-es/common-logic/transaction/split.d.ts.map +1 -0
  136. package/lib-es/common-logic/transaction/split.js +83 -0
  137. package/lib-es/common-logic/transaction/split.js.map +1 -0
  138. package/lib-es/common-logic/utils.js +2 -2
  139. package/lib-es/common-logic/utils.js.map +1 -1
  140. package/lib-es/network/gateway.d.ts +6 -1
  141. package/lib-es/network/gateway.d.ts.map +1 -1
  142. package/lib-es/network/gateway.js +5 -10
  143. package/lib-es/network/gateway.js.map +1 -1
  144. package/lib-es/test/cantonTestUtils.d.ts +4 -9
  145. package/lib-es/test/cantonTestUtils.d.ts.map +1 -1
  146. package/lib-es/test/cantonTestUtils.js +697 -21
  147. package/lib-es/test/cantonTestUtils.js.map +1 -1
  148. package/lib-es/test/fixtures.d.ts +5 -0
  149. package/lib-es/test/fixtures.d.ts.map +1 -0
  150. package/lib-es/test/fixtures.js +49 -0
  151. package/lib-es/test/fixtures.js.map +1 -0
  152. package/lib-es/types/bridge.d.ts +14 -2
  153. package/lib-es/types/bridge.d.ts.map +1 -1
  154. package/lib-es/types/errors.d.ts +6 -0
  155. package/lib-es/types/errors.d.ts.map +1 -1
  156. package/lib-es/types/errors.js +2 -0
  157. package/lib-es/types/errors.js.map +1 -1
  158. package/lib-es/types/index.d.ts +1 -0
  159. package/lib-es/types/index.d.ts.map +1 -1
  160. package/lib-es/types/index.js +1 -0
  161. package/lib-es/types/index.js.map +1 -1
  162. package/lib-es/types/signer.d.ts +15 -2
  163. package/lib-es/types/signer.d.ts.map +1 -1
  164. package/lib-es/types/transaction-proto.json +1238 -0
  165. package/package.json +13 -10
  166. package/scripts/generate.js +261 -0
  167. package/src/bridge/deviceTransactionConfig.test.ts +14 -10
  168. package/src/bridge/deviceTransactionConfig.ts +2 -2
  169. package/src/bridge/estimateMaxSpendable.ts +6 -8
  170. package/src/bridge/getTransactionStatus.test.ts +103 -165
  171. package/src/bridge/getTransactionStatus.ts +43 -11
  172. package/src/bridge/index.ts +6 -5
  173. package/src/bridge/onboard.integ.test.ts +8 -51
  174. package/src/bridge/onboard.ts +58 -33
  175. package/src/bridge/prepareTransaction.ts +1 -1
  176. package/src/bridge/serialization.ts +44 -0
  177. package/src/bridge/signOperation.test.ts +123 -0
  178. package/src/bridge/signOperation.ts +13 -6
  179. package/src/bridge/sync.integ.test.ts +157 -132
  180. package/src/bridge/sync.test.ts +5 -1
  181. package/src/bridge/sync.ts +18 -7
  182. package/src/bridge/transaction.ts +1 -1
  183. package/src/common-logic/account/getBalance.ts +12 -2
  184. package/src/common-logic/account/getBalance.unit.test.ts +7 -1
  185. package/src/common-logic/index.ts +1 -0
  186. package/src/common-logic/transaction/craftTransaction.ts +1 -1
  187. package/src/common-logic/transaction/estimateFees.test.ts +10 -0
  188. package/src/common-logic/transaction/estimateFees.ts +1 -1
  189. package/src/common-logic/transaction/sign.test.ts +389 -0
  190. package/src/common-logic/transaction/sign.ts +59 -0
  191. package/src/common-logic/transaction/split.test.ts +50 -0
  192. package/src/common-logic/transaction/split.ts +101 -0
  193. package/src/common-logic/utils.test.ts +22 -30
  194. package/src/common-logic/utils.ts +2 -2
  195. package/src/network/gateway.integ.test.ts +5 -6
  196. package/src/network/gateway.ts +13 -10
  197. package/src/test/cantonTestUtils.ts +789 -24
  198. package/src/test/fixtures.ts +53 -0
  199. package/src/test/prepare-transfer-serialized.json +26 -0
  200. package/src/test/prepare-transfer.json +3298 -0
  201. package/src/types/bridge.ts +15 -2
  202. package/src/types/errors.ts +3 -0
  203. package/src/types/index.ts +1 -0
  204. package/src/types/signer.ts +21 -3
  205. package/src/types/transaction-proto.json +1238 -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::123",
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",
@@ -89,11 +47,11 @@ describe("getTransactionStatus", () => {
89
47
  });
90
48
 
91
49
  describe("fee validation", () => {
92
- it("should return FeeNotLoaded error when fee is not provided", async () => {
50
+ it("should return FeeNotLoaded error when fee is null", async () => {
93
51
  const transaction: Transaction = {
94
52
  family: "canton",
95
53
  amount: new BigNumber(100),
96
- recipient: "valid::123",
54
+ recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
97
55
  fee: null,
98
56
  tokenId: "",
99
57
  };
@@ -104,27 +62,27 @@ describe("getTransactionStatus", () => {
104
62
  expect(result.warnings).toEqual({});
105
63
  });
106
64
 
107
- it("should return FeeRequired error when fee is zero", async () => {
65
+ it("should return FeeNotLoaded error when fee is undefined", async () => {
108
66
  const transaction: Transaction = {
109
67
  family: "canton",
110
68
  amount: new BigNumber(100),
111
- recipient: "valid::123",
112
- fee: new BigNumber(0),
69
+ recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
70
+ fee: undefined,
113
71
  tokenId: "",
114
72
  };
115
73
 
116
74
  const result = await getTransactionStatus(mockAccount, transaction);
117
75
 
118
- expect(result.errors.fee).toBeInstanceOf(FeeRequired);
76
+ expect(result.errors.fee).toBeInstanceOf(FeeNotLoaded);
119
77
  expect(result.warnings).toEqual({});
120
78
  });
121
79
 
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
126
- recipient: "valid::123",
127
- fee: new BigNumber(1500), // 15x the amount
83
+ amount: new BigNumber(100),
84
+ recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
85
+ fee: new BigNumber(1500),
128
86
  tokenId: "",
129
87
  };
130
88
 
@@ -138,8 +96,8 @@ describe("getTransactionStatus", () => {
138
96
  const transaction: Transaction = {
139
97
  family: "canton",
140
98
  amount: new BigNumber(100),
141
- recipient: "valid::123",
142
- fee: new BigNumber(10), // 0.1x the amount
99
+ recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
100
+ fee: new BigNumber(10),
143
101
  tokenId: "",
144
102
  };
145
103
 
@@ -154,8 +112,10 @@ 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
158
- recipient: "valid::123",
115
+ amount: mockAccount.balance
116
+ .minus(new BigNumber(mockCoinConfig.getCoinConfig().minReserve))
117
+ .plus(1),
118
+ recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
159
119
  fee: new BigNumber(10),
160
120
  tokenId: "",
161
121
  };
@@ -168,8 +128,8 @@ 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
172
- recipient: "valid::123",
131
+ amount: new BigNumber(mockCoinConfig.getCoinConfig().minReserve).minus(1),
132
+ recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
173
133
  fee: new BigNumber(10),
174
134
  tokenId: "",
175
135
  };
@@ -182,8 +142,8 @@ 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
186
- recipient: "valid::123",
145
+ amount: mockAccount.balance.multipliedBy(0.5),
146
+ recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
187
147
  fee: new BigNumber(10),
188
148
  tokenId: "",
189
149
  };
@@ -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::123", // 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 () => {
@@ -241,7 +201,7 @@ describe("getTransactionStatus", () => {
241
201
  const transaction: Transaction = {
242
202
  family: "canton",
243
203
  amount: new BigNumber(100),
244
- recipient: "valid::456",
204
+ recipient: "valid::22222222222222222222222222222222222222222222222222222222222222222222",
245
205
  fee: new BigNumber(10),
246
206
  tokenId: "",
247
207
  };
@@ -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",
@@ -272,12 +224,12 @@ describe("getTransactionStatus", () => {
272
224
  const transaction: Transaction = {
273
225
  family: "canton",
274
226
  amount: new BigNumber(0),
275
- recipient: "valid::123",
227
+ recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
276
228
  fee: new BigNumber(10),
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
  });
@@ -286,7 +238,7 @@ describe("getTransactionStatus", () => {
286
238
  const transaction: Transaction = {
287
239
  family: "canton",
288
240
  amount: new BigNumber(100),
289
- recipient: "valid::123",
241
+ recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
290
242
  fee: new BigNumber(10),
291
243
  tokenId: "",
292
244
  };
@@ -302,7 +254,7 @@ describe("getTransactionStatus", () => {
302
254
  const transaction: Transaction = {
303
255
  family: "canton",
304
256
  amount: new BigNumber(100),
305
- recipient: "valid::123",
257
+ recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
306
258
  fee: new BigNumber(10),
307
259
  tokenId: "",
308
260
  };
@@ -318,7 +270,7 @@ describe("getTransactionStatus", () => {
318
270
  const transaction: Transaction = {
319
271
  family: "canton",
320
272
  amount: new BigNumber(100),
321
- recipient: "valid::123",
273
+ recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
322
274
  fee: new BigNumber(10),
323
275
  tokenId: "",
324
276
  };
@@ -330,117 +282,103 @@ 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 = {
341
297
  family: "canton",
342
298
  amount: new BigNumber(50),
343
- recipient: "valid::123",
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 = {
360
321
  family: "canton",
361
322
  amount: new BigNumber(50),
362
- recipient: "valid::123",
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::123",
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
- recipient: "valid::123",
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::123",
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> = {};
@@ -35,12 +44,9 @@ export const getTransactionStatus: AccountBridge<
35
44
  warnings.feeTooHigh = new FeeTooHigh();
36
45
  }
37
46
 
38
- if (!transaction.fee) {
47
+ if (transaction.fee === null || transaction.fee === undefined) {
39
48
  // if the fee is not loaded, we can't do much
40
49
  errors.fee = new FeeNotLoaded();
41
- } else if (transaction.fee.eq(0)) {
42
- // On some chains, 0 fee could still work so this is optional
43
- errors.fee = new FeeRequired();
44
50
  } else if (totalSpent.gt(account.balance.minus(reserveAmount))) {
45
51
  // if the total spent is greater than the balance minus the reserve amount, tx is invalid
46
52
  errors.amount = new NotEnoughSpendableBalance("", {
@@ -63,9 +69,6 @@ export const getTransactionStatus: AccountBridge<
63
69
 
64
70
  if (!transaction.recipient) {
65
71
  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
72
  } else if (!isRecipientValid(transaction.recipient)) {
70
73
  // We want to prevent user from sending to an invalid address
71
74
  errors.recipient = new InvalidAddress("", {
@@ -78,6 +81,8 @@ export const getTransactionStatus: AccountBridge<
78
81
  errors.amount = new AmountRequired();
79
82
  }
80
83
 
84
+ validateUtxoCount(account, transaction, warnings);
85
+
81
86
  return {
82
87
  errors,
83
88
  warnings,
@@ -86,3 +91,30 @@ export const getTransactionStatus: AccountBridge<
86
91
  totalSpent,
87
92
  };
88
93
  };
94
+
95
+ function validateUtxoCount(
96
+ account: CantonAccount,
97
+ transaction: Transaction,
98
+ warnings: Record<string, Error>,
99
+ ): void {
100
+ const abandonSeedAddress = getAbandonSeedAddress(account.currency.id);
101
+ const isAbandonSeedAddress = transaction.recipient?.includes(abandonSeedAddress);
102
+
103
+ // UTXO count validation - only validate if recipient is valid and not equal to sender
104
+ // Skip validation for abandon seed addresses
105
+ if (
106
+ account?.cantonResources?.instrumentUtxoCounts &&
107
+ transaction.recipient &&
108
+ isRecipientValid(transaction.recipient) &&
109
+ account.xpub !== transaction.recipient &&
110
+ !isAbandonSeedAddress
111
+ ) {
112
+ const { instrumentUtxoCounts } = account.cantonResources;
113
+ const instrumentUtxoCount = instrumentUtxoCounts[transaction.tokenId] || 0;
114
+ if (instrumentUtxoCount > TO_MANY_UTXOS_CRITICAL_COUNT) {
115
+ warnings.tooManyUtxos = new TooManyUtxosCritical();
116
+ } else if (instrumentUtxoCount > TO_MANY_UTXOS_WARNING_COUNT) {
117
+ warnings.tooManyUtxos = new TooManyUtxosWarning("families.canton.tooManyUtxos.warning");
118
+ }
119
+ }
120
+ }
@@ -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>;
@@ -221,25 +198,5 @@ describe("onboard (devnet)", () => {
221
198
  expect(finalResult.isApproved).toBe(true);
222
199
  expect(typeof finalResult.isApproved).toBe("boolean");
223
200
  }, 30000);
224
-
225
- it("should handle invalid party ID gracefully", async () => {
226
- // GIVEN
227
- const keyPair = generateMockKeyPair();
228
- const mockSigner = createMockSigner(keyPair);
229
- const mockSignerContext = jest.fn().mockImplementation((deviceId, callback) => {
230
- return callback(mockSigner);
231
- });
232
- const preapprovalObservable = buildAuthorizePreapproval(mockSignerContext);
233
-
234
- // WHEN & THEN
235
- try {
236
- await firstValueFrom(
237
- preapprovalObservable(mockCurrency, mockDeviceId, mockAccount, "invalid-party-id-123"),
238
- );
239
- expect(true).toBe(true);
240
- } catch (error) {
241
- expect(error).toBeDefined();
242
- }
243
- }, 30000);
244
201
  });
245
202
  });