@ledgerhq/coin-canton 0.10.0 → 0.11.0-nightly.20251126160702

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 (69) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/lib/bridge/acceptOffer.d.ts +8 -0
  3. package/lib/bridge/acceptOffer.d.ts.map +1 -0
  4. package/lib/bridge/acceptOffer.js +18 -0
  5. package/lib/bridge/acceptOffer.js.map +1 -0
  6. package/lib/bridge/index.d.ts.map +1 -1
  7. package/lib/bridge/index.js +3 -0
  8. package/lib/bridge/index.js.map +1 -1
  9. package/lib/bridge/serialization.d.ts +2 -0
  10. package/lib/bridge/serialization.d.ts.map +1 -1
  11. package/lib/bridge/serialization.js +4 -1
  12. package/lib/bridge/serialization.js.map +1 -1
  13. package/lib/bridge/sync.d.ts.map +1 -1
  14. package/lib/bridge/sync.js +14 -4
  15. package/lib/bridge/sync.js.map +1 -1
  16. package/lib/helpers.d.ts +10 -0
  17. package/lib/helpers.d.ts.map +1 -0
  18. package/lib/helpers.js +26 -0
  19. package/lib/helpers.js.map +1 -0
  20. package/lib/index.d.ts +2 -0
  21. package/lib/index.d.ts.map +1 -1
  22. package/lib/index.js +5 -1
  23. package/lib/index.js.map +1 -1
  24. package/lib/network/gateway.d.ts +18 -0
  25. package/lib/network/gateway.d.ts.map +1 -1
  26. package/lib/network/gateway.js +26 -6
  27. package/lib/network/gateway.js.map +1 -1
  28. package/lib/types/bridge.d.ts +4 -0
  29. package/lib/types/bridge.d.ts.map +1 -1
  30. package/lib-es/bridge/acceptOffer.d.ts +8 -0
  31. package/lib-es/bridge/acceptOffer.d.ts.map +1 -0
  32. package/lib-es/bridge/acceptOffer.js +14 -0
  33. package/lib-es/bridge/acceptOffer.js.map +1 -0
  34. package/lib-es/bridge/index.d.ts.map +1 -1
  35. package/lib-es/bridge/index.js +3 -0
  36. package/lib-es/bridge/index.js.map +1 -1
  37. package/lib-es/bridge/serialization.d.ts +2 -0
  38. package/lib-es/bridge/serialization.d.ts.map +1 -1
  39. package/lib-es/bridge/serialization.js +4 -2
  40. package/lib-es/bridge/serialization.js.map +1 -1
  41. package/lib-es/bridge/sync.d.ts.map +1 -1
  42. package/lib-es/bridge/sync.js +16 -6
  43. package/lib-es/bridge/sync.js.map +1 -1
  44. package/lib-es/helpers.d.ts +10 -0
  45. package/lib-es/helpers.d.ts.map +1 -0
  46. package/lib-es/helpers.js +22 -0
  47. package/lib-es/helpers.js.map +1 -0
  48. package/lib-es/index.d.ts +2 -0
  49. package/lib-es/index.d.ts.map +1 -1
  50. package/lib-es/index.js +2 -0
  51. package/lib-es/index.js.map +1 -1
  52. package/lib-es/network/gateway.d.ts +18 -0
  53. package/lib-es/network/gateway.d.ts.map +1 -1
  54. package/lib-es/network/gateway.js +22 -6
  55. package/lib-es/network/gateway.js.map +1 -1
  56. package/lib-es/types/bridge.d.ts +4 -0
  57. package/lib-es/types/bridge.d.ts.map +1 -1
  58. package/package.json +6 -6
  59. package/src/bridge/acceptOffer.test.ts +300 -0
  60. package/src/bridge/acceptOffer.ts +36 -0
  61. package/src/bridge/getTransactionStatus.test.ts +1 -0
  62. package/src/bridge/index.ts +3 -0
  63. package/src/bridge/serialization.ts +4 -2
  64. package/src/bridge/sync.ts +21 -6
  65. package/src/helpers.test.ts +361 -0
  66. package/src/helpers.ts +31 -0
  67. package/src/index.ts +2 -0
  68. package/src/network/gateway.ts +60 -6
  69. package/src/types/bridge.ts +15 -0
@@ -0,0 +1,361 @@
1
+ import BigNumber from "bignumber.js";
2
+ import { TokenAccount } from "@ledgerhq/types-live";
3
+ import { isAccountEmpty } from "./helpers";
4
+ import { CantonAccount } from "./types";
5
+ import { createMockAccount } from "./test/fixtures";
6
+
7
+ describe("isAccountEmpty", () => {
8
+ const createCantonAccount = (overrides: Partial<CantonAccount> = {}): CantonAccount => {
9
+ const baseAccount = createMockAccount(overrides);
10
+ return {
11
+ ...baseAccount,
12
+ cantonResources: {
13
+ instrumentUtxoCounts: {},
14
+ pendingTransferProposals: [],
15
+ ...overrides.cantonResources,
16
+ },
17
+ };
18
+ };
19
+
20
+ describe("when account is not of type Account", () => {
21
+ it("should return false for TokenAccount", () => {
22
+ const tokenAccount: TokenAccount = {
23
+ type: "TokenAccount",
24
+ id: "token-account-id",
25
+ parentId: "parent-account-id",
26
+ token: {
27
+ type: "TokenCurrency",
28
+ id: "token-id",
29
+ contractAddress: "0x123",
30
+ name: "Test Token",
31
+ ticker: "TEST",
32
+ decimals: 18,
33
+ parentCurrency: {
34
+ id: "ethereum",
35
+ type: "CryptoCurrency",
36
+ name: "Ethereum",
37
+ ticker: "ETH",
38
+ family: "evm",
39
+ units: [],
40
+ explorerViews: [],
41
+ },
42
+ },
43
+ balance: new BigNumber(0),
44
+ spendableBalance: new BigNumber(0),
45
+ operationsCount: 0,
46
+ operations: [],
47
+ pendingOperations: [],
48
+ balanceHistoryCache: {
49
+ HOUR: { latestDate: null, balances: [] },
50
+ DAY: { latestDate: null, balances: [] },
51
+ WEEK: { latestDate: null, balances: [] },
52
+ },
53
+ swapHistory: [],
54
+ creationDate: new Date(),
55
+ balanceHistory: [],
56
+ };
57
+
58
+ expect(isAccountEmpty(tokenAccount)).toBe(false);
59
+ });
60
+ });
61
+
62
+ describe("when account is not a Canton account", () => {
63
+ it("should return false for non-Canton account", () => {
64
+ const nonCantonAccount = createMockAccount({
65
+ currency: {
66
+ ...createMockAccount().currency,
67
+ family: "ethereum",
68
+ },
69
+ });
70
+
71
+ expect(isAccountEmpty(nonCantonAccount)).toBe(false);
72
+ });
73
+ });
74
+
75
+ describe("when account is a Canton account", () => {
76
+ it("should return true for empty account (zero operations, zero balance, no subAccounts, no pending proposals)", () => {
77
+ const emptyAccount = createCantonAccount({
78
+ operationsCount: 0,
79
+ balance: new BigNumber(0),
80
+ subAccounts: [],
81
+ cantonResources: {
82
+ instrumentUtxoCounts: {},
83
+ pendingTransferProposals: [],
84
+ },
85
+ });
86
+
87
+ expect(isAccountEmpty(emptyAccount)).toBe(true);
88
+ });
89
+
90
+ it("should return false when account has operations", () => {
91
+ const accountWithOperations = createCantonAccount({
92
+ operationsCount: 1,
93
+ balance: new BigNumber(0),
94
+ subAccounts: [],
95
+ cantonResources: {
96
+ instrumentUtxoCounts: {},
97
+ pendingTransferProposals: [],
98
+ },
99
+ });
100
+
101
+ expect(isAccountEmpty(accountWithOperations)).toBe(false);
102
+ });
103
+
104
+ it("should return false when account has non-zero balance", () => {
105
+ const accountWithBalance = createCantonAccount({
106
+ operationsCount: 0,
107
+ balance: new BigNumber(100),
108
+ subAccounts: [],
109
+ cantonResources: {
110
+ instrumentUtxoCounts: {},
111
+ pendingTransferProposals: [],
112
+ },
113
+ });
114
+
115
+ expect(isAccountEmpty(accountWithBalance)).toBe(false);
116
+ });
117
+
118
+ it("should return false when account has subAccounts", () => {
119
+ const accountWithSubAccounts = createCantonAccount({
120
+ operationsCount: 0,
121
+ balance: new BigNumber(0),
122
+ subAccounts: [
123
+ {
124
+ type: "TokenAccount",
125
+ id: "token-account-id",
126
+ parentId: "parent-id",
127
+ token: {
128
+ type: "TokenCurrency",
129
+ id: "token-id",
130
+ contractAddress: "0x123",
131
+ name: "Test Token",
132
+ ticker: "TEST",
133
+ decimals: 18,
134
+ parentCurrency: createMockAccount().currency,
135
+ },
136
+ balance: new BigNumber(0),
137
+ spendableBalance: new BigNumber(0),
138
+ operationsCount: 0,
139
+ operations: [],
140
+ pendingOperations: [],
141
+ balanceHistoryCache: {
142
+ HOUR: { latestDate: null, balances: [] },
143
+ DAY: { latestDate: null, balances: [] },
144
+ WEEK: { latestDate: null, balances: [] },
145
+ },
146
+ swapHistory: [],
147
+ creationDate: new Date(),
148
+ balanceHistory: [],
149
+ },
150
+ ],
151
+ cantonResources: {
152
+ instrumentUtxoCounts: {},
153
+ pendingTransferProposals: [],
154
+ },
155
+ });
156
+
157
+ expect(isAccountEmpty(accountWithSubAccounts)).toBe(false);
158
+ });
159
+
160
+ it("should return false when account has pending transfer proposals", () => {
161
+ const accountWithPendingProposals = createCantonAccount({
162
+ operationsCount: 0,
163
+ balance: new BigNumber(0),
164
+ subAccounts: [],
165
+ cantonResources: {
166
+ instrumentUtxoCounts: {},
167
+ pendingTransferProposals: [
168
+ {
169
+ contract_id: "contract-123",
170
+ sender: "sender-address",
171
+ receiver: "receiver-address",
172
+ instrument_id: "instrument-123",
173
+ amount: "100",
174
+ memo: "test memo",
175
+ expires_at_micros: Date.now() * 1000 + 86400000000,
176
+ },
177
+ ],
178
+ },
179
+ });
180
+
181
+ expect(isAccountEmpty(accountWithPendingProposals)).toBe(false);
182
+ });
183
+
184
+ it("should return false when account has operations and balance", () => {
185
+ const accountWithBoth = createCantonAccount({
186
+ operationsCount: 5,
187
+ balance: new BigNumber(1000),
188
+ subAccounts: [],
189
+ cantonResources: {
190
+ instrumentUtxoCounts: {},
191
+ pendingTransferProposals: [],
192
+ },
193
+ });
194
+
195
+ expect(isAccountEmpty(accountWithBoth)).toBe(false);
196
+ });
197
+
198
+ it("should return false when account has operations and subAccounts", () => {
199
+ const accountWithOperationsAndSubAccounts = createCantonAccount({
200
+ operationsCount: 2,
201
+ balance: new BigNumber(0),
202
+ subAccounts: [
203
+ {
204
+ type: "TokenAccount",
205
+ id: "token-account-id",
206
+ parentId: "parent-id",
207
+ token: {
208
+ type: "TokenCurrency",
209
+ id: "token-id",
210
+ contractAddress: "0x123",
211
+ name: "Test Token",
212
+ ticker: "TEST",
213
+ decimals: 18,
214
+ parentCurrency: createMockAccount().currency,
215
+ },
216
+ balance: new BigNumber(0),
217
+ spendableBalance: new BigNumber(0),
218
+ operationsCount: 0,
219
+ operations: [],
220
+ pendingOperations: [],
221
+ balanceHistoryCache: {
222
+ HOUR: { latestDate: null, balances: [] },
223
+ DAY: { latestDate: null, balances: [] },
224
+ WEEK: { latestDate: null, balances: [] },
225
+ },
226
+ swapHistory: [],
227
+ creationDate: new Date(),
228
+ balanceHistory: [],
229
+ },
230
+ ],
231
+ cantonResources: {
232
+ instrumentUtxoCounts: {},
233
+ pendingTransferProposals: [],
234
+ },
235
+ });
236
+
237
+ expect(isAccountEmpty(accountWithOperationsAndSubAccounts)).toBe(false);
238
+ });
239
+
240
+ it("should return false when account has balance and pending proposals", () => {
241
+ const accountWithBalanceAndProposals = createCantonAccount({
242
+ operationsCount: 0,
243
+ balance: new BigNumber(500),
244
+ subAccounts: [],
245
+ cantonResources: {
246
+ instrumentUtxoCounts: {},
247
+ pendingTransferProposals: [
248
+ {
249
+ contract_id: "contract-456",
250
+ sender: "sender-address",
251
+ receiver: "receiver-address",
252
+ instrument_id: "instrument-456",
253
+ amount: "50",
254
+ memo: "test memo",
255
+ expires_at_micros: Date.now() * 1000 + 86400000000, // 1 day from now
256
+ },
257
+ ],
258
+ },
259
+ });
260
+
261
+ expect(isAccountEmpty(accountWithBalanceAndProposals)).toBe(false);
262
+ });
263
+
264
+ it("should return false when account has all non-empty indicators", () => {
265
+ const fullyUsedAccount = createCantonAccount({
266
+ operationsCount: 10,
267
+ balance: new BigNumber(10000),
268
+ subAccounts: [
269
+ {
270
+ type: "TokenAccount",
271
+ id: "token-account-id",
272
+ parentId: "parent-id",
273
+ token: {
274
+ type: "TokenCurrency",
275
+ id: "token-id",
276
+ contractAddress: "0x123",
277
+ name: "Test Token",
278
+ ticker: "TEST",
279
+ decimals: 18,
280
+ parentCurrency: createMockAccount().currency,
281
+ },
282
+ balance: new BigNumber(100),
283
+ spendableBalance: new BigNumber(100),
284
+ operationsCount: 5,
285
+ operations: [],
286
+ pendingOperations: [],
287
+ balanceHistoryCache: {
288
+ HOUR: { latestDate: null, balances: [] },
289
+ DAY: { latestDate: null, balances: [] },
290
+ WEEK: { latestDate: null, balances: [] },
291
+ },
292
+ swapHistory: [],
293
+ creationDate: new Date(),
294
+ balanceHistory: [],
295
+ },
296
+ ],
297
+ cantonResources: {
298
+ instrumentUtxoCounts: {
299
+ "instrument-1": 5,
300
+ "instrument-2": 3,
301
+ },
302
+ pendingTransferProposals: [
303
+ {
304
+ contract_id: "contract-789",
305
+ sender: "sender-address",
306
+ receiver: "receiver-address",
307
+ instrument_id: "instrument-789",
308
+ amount: "200",
309
+ memo: "test memo",
310
+ expires_at_micros: Date.now() * 1000 + 86400000000, // 1 day from now
311
+ },
312
+ ],
313
+ },
314
+ });
315
+
316
+ expect(isAccountEmpty(fullyUsedAccount)).toBe(false);
317
+ });
318
+
319
+ it("should handle undefined subAccounts", () => {
320
+ const accountWithUndefinedSubAccounts = createCantonAccount({
321
+ operationsCount: 0,
322
+ balance: new BigNumber(0),
323
+ subAccounts: undefined,
324
+ cantonResources: {
325
+ instrumentUtxoCounts: {},
326
+ pendingTransferProposals: [],
327
+ },
328
+ });
329
+
330
+ expect(isAccountEmpty(accountWithUndefinedSubAccounts)).toBe(true);
331
+ });
332
+
333
+ it("should handle empty subAccounts array", () => {
334
+ const accountWithEmptySubAccounts = createCantonAccount({
335
+ operationsCount: 0,
336
+ balance: new BigNumber(0),
337
+ subAccounts: [],
338
+ cantonResources: {
339
+ instrumentUtxoCounts: {},
340
+ pendingTransferProposals: [],
341
+ },
342
+ });
343
+
344
+ expect(isAccountEmpty(accountWithEmptySubAccounts)).toBe(true);
345
+ });
346
+
347
+ it("should handle empty pendingTransferProposals array", () => {
348
+ const accountWithEmptyProposals = createCantonAccount({
349
+ operationsCount: 0,
350
+ balance: new BigNumber(0),
351
+ subAccounts: [],
352
+ cantonResources: {
353
+ instrumentUtxoCounts: {},
354
+ pendingTransferProposals: [],
355
+ },
356
+ });
357
+
358
+ expect(isAccountEmpty(accountWithEmptyProposals)).toBe(true);
359
+ });
360
+ });
361
+ });
package/src/helpers.ts ADDED
@@ -0,0 +1,31 @@
1
+ import type { AccountLike } from "@ledgerhq/types-live";
2
+ import { isCantonAccount } from "./bridge/serialization";
3
+ import { CantonAccount } from "./types";
4
+
5
+ export function isCantonAccountEmpty(
6
+ account: Pick<CantonAccount, "operationsCount" | "balance" | "subAccounts" | "cantonResources">,
7
+ ): boolean {
8
+ return (
9
+ account.operationsCount === 0 &&
10
+ account.balance.isZero() &&
11
+ !account.subAccounts?.length &&
12
+ !account.cantonResources?.pendingTransferProposals?.length
13
+ );
14
+ }
15
+
16
+ /**
17
+ * Check if a Canton account is empty (has no operations, no balance, and no pending transfer proposals)
18
+ * @param account - The account to check (must be a Canton account)
19
+ * @returns true if the account is empty, false otherwise
20
+ */
21
+ export function isAccountEmpty(account: AccountLike): boolean {
22
+ if (account.type !== "Account") {
23
+ return false;
24
+ }
25
+
26
+ if (!isCantonAccount(account)) {
27
+ return false;
28
+ }
29
+
30
+ return isCantonAccountEmpty(account);
31
+ }
package/src/index.ts CHANGED
@@ -2,3 +2,5 @@ export * from "./types";
2
2
 
3
3
  export { createBridges } from "./bridge/index";
4
4
  export type { CantonCoinConfig } from "./config";
5
+ export { isAccountEmpty } from "./helpers";
6
+ export { isCantonAccount } from "./bridge/serialization";
@@ -58,6 +58,25 @@ export type PrepareTransferRequest = {
58
58
  reason?: string;
59
59
  };
60
60
 
61
+ export type PrepareTransferInstructionRequest = {
62
+ type:
63
+ | "accept-transfer-instruction"
64
+ | "reject-transfer-instruction"
65
+ | "withdraw-transfer-instruction";
66
+ contract_id: string;
67
+ reason?: string;
68
+ };
69
+
70
+ export type TransferProposal = {
71
+ contract_id: string;
72
+ sender: string;
73
+ receiver: string;
74
+ amount: string;
75
+ instrument_id: string;
76
+ memo: string;
77
+ expires_at_micros: number;
78
+ };
79
+
61
80
  type OnboardingSubmitRequest = {
62
81
  prepare_request: OnboardingPrepareRequest;
63
82
  prepare_response: OnboardingPrepareResponse;
@@ -380,6 +399,22 @@ export async function submit(
380
399
  return data;
381
400
  }
382
401
 
402
+ export async function prepare(
403
+ currency: CryptoCurrency,
404
+ partyId: string,
405
+ params: PrepareTransferRequest | PrepareTransferInstructionRequest,
406
+ ) {
407
+ const { data } = await gatewayNetwork<
408
+ PrepareTransferResponse,
409
+ PrepareTransferRequest | PrepareTransferInstructionRequest
410
+ >({
411
+ method: "POST",
412
+ url: `${getGatewayUrl(currency)}/v1/node/${getNodeId(currency)}/party/${partyId}/transaction/prepare`,
413
+ data: params,
414
+ });
415
+ return data;
416
+ }
417
+
383
418
  export async function getBalance(currency: CryptoCurrency, partyId: string) {
384
419
  const { data } = await gatewayNetwork<GetBalanceResponse>({
385
420
  method: "GET",
@@ -502,13 +537,15 @@ export async function prepareTransferRequest(
502
537
  partyId: string,
503
538
  params: PrepareTransferRequest,
504
539
  ) {
505
- const { data } = await gatewayNetwork<PrepareTransferResponse, PrepareTransferRequest>({
506
- method: "POST",
507
- url: `${getGatewayUrl(currency)}/v1/node/${getNodeId(currency)}/party/${partyId}/transaction/prepare`,
508
- data: params,
509
- });
540
+ return prepare(currency, partyId, params);
541
+ }
510
542
 
511
- return data;
543
+ export async function prepareTransferInstruction(
544
+ currency: CryptoCurrency,
545
+ partyId: string,
546
+ params: PrepareTransferInstructionRequest,
547
+ ) {
548
+ return prepare(currency, partyId, params);
512
549
  }
513
550
 
514
551
  export async function getLedgerEnd(currency: CryptoCurrency): Promise<number> {
@@ -553,6 +590,15 @@ export async function submitPreApprovalTransaction(
553
590
  } satisfies PreApprovalResult;
554
591
  }
555
592
 
593
+ export async function submitTransferInstruction(
594
+ currency: CryptoCurrency,
595
+ partyId: string,
596
+ serialized: string,
597
+ signature: string,
598
+ ) {
599
+ return submit(currency, partyId, serialized, signature);
600
+ }
601
+
556
602
  type GetTransferPreApprovalResponse = {
557
603
  contract_id: string;
558
604
  receiver: string;
@@ -569,3 +615,11 @@ export async function getTransferPreApproval(currency: CryptoCurrency, partyId:
569
615
  });
570
616
  return data;
571
617
  }
618
+
619
+ export async function getPendingTransferProposals(currency: CryptoCurrency, partyId: string) {
620
+ const { data } = await gatewayNetwork<TransferProposal[]>({
621
+ method: "GET",
622
+ url: `${getGatewayUrl(currency)}/v1/node/${getNodeId(currency)}/party/${partyId}/transfer-proposals?timestamp=${Date.now()}`,
623
+ });
624
+ return data;
625
+ }
@@ -16,6 +16,7 @@ import type {
16
16
  CantonAuthorizeProgress,
17
17
  CantonAuthorizeResult,
18
18
  } from "./onboard";
19
+ import type { TransferProposal } from "../network/gateway";
19
20
 
20
21
  export interface CantonCurrencyBridge extends CurrencyBridge {
21
22
  onboardAccount: (
@@ -29,6 +30,18 @@ export interface CantonCurrencyBridge extends CurrencyBridge {
29
30
  creatableAccount: Account,
30
31
  partyId: string,
31
32
  ) => Observable<CantonAuthorizeProgress | CantonAuthorizeResult>;
33
+ transferInstruction: (
34
+ currency: CryptoCurrency,
35
+ deviceId: string,
36
+ account: Account,
37
+ partyId: string,
38
+ contractId: string,
39
+ type:
40
+ | "accept-transfer-instruction"
41
+ | "reject-transfer-instruction"
42
+ | "withdraw-transfer-instruction",
43
+ reason?: string,
44
+ ) => Promise<void>;
32
45
  }
33
46
 
34
47
  export type NetworkInfo = {
@@ -64,9 +77,11 @@ export type TransactionStatusRaw = TransactionStatusCommonRaw;
64
77
 
65
78
  export type CantonResources = {
66
79
  instrumentUtxoCounts: Record<string, number>;
80
+ pendingTransferProposals: TransferProposal[];
67
81
  };
68
82
  export type CantonResourcesRaw = {
69
83
  instrumentUtxoCounts: Record<string, number>;
84
+ pendingTransferProposals: TransferProposal[];
70
85
  };
71
86
 
72
87
  export type CantonAccount = Account & {