@ledgerhq/coin-canton 0.11.0 → 0.12.0-nightly.20251210100832

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 (84) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +1 -1
  3. package/lib/bridge/acceptOffer.d.ts.map +1 -1
  4. package/lib/bridge/acceptOffer.js +8 -0
  5. package/lib/bridge/acceptOffer.js.map +1 -1
  6. package/lib/bridge/getTransactionStatus.d.ts +1 -0
  7. package/lib/bridge/getTransactionStatus.d.ts.map +1 -1
  8. package/lib/bridge/getTransactionStatus.js +48 -17
  9. package/lib/bridge/getTransactionStatus.js.map +1 -1
  10. package/lib/bridge/onboard.d.ts.map +1 -1
  11. package/lib/bridge/onboard.js +4 -1
  12. package/lib/bridge/onboard.js.map +1 -1
  13. package/lib/bridge/serialization.d.ts.map +1 -1
  14. package/lib/bridge/serialization.js +7 -1
  15. package/lib/bridge/serialization.js.map +1 -1
  16. package/lib/bridge/sync.d.ts.map +1 -1
  17. package/lib/bridge/sync.js +19 -14
  18. package/lib/bridge/sync.js.map +1 -1
  19. package/lib/network/gateway.d.ts +20 -3
  20. package/lib/network/gateway.d.ts.map +1 -1
  21. package/lib/network/gateway.js +75 -16
  22. package/lib/network/gateway.js.map +1 -1
  23. package/lib/test/cantonTestUtils.d.ts +3 -1
  24. package/lib/test/cantonTestUtils.d.ts.map +1 -1
  25. package/lib/test/cantonTestUtils.js +1 -1
  26. package/lib/test/cantonTestUtils.js.map +1 -1
  27. package/lib/types/bridge.d.ts +6 -0
  28. package/lib/types/bridge.d.ts.map +1 -1
  29. package/lib/types/errors.d.ts +3 -0
  30. package/lib/types/errors.d.ts.map +1 -1
  31. package/lib/types/errors.js +2 -1
  32. package/lib/types/errors.js.map +1 -1
  33. package/lib-es/bridge/acceptOffer.d.ts.map +1 -1
  34. package/lib-es/bridge/acceptOffer.js +8 -0
  35. package/lib-es/bridge/acceptOffer.js.map +1 -1
  36. package/lib-es/bridge/getTransactionStatus.d.ts +1 -0
  37. package/lib-es/bridge/getTransactionStatus.d.ts.map +1 -1
  38. package/lib-es/bridge/getTransactionStatus.js +47 -17
  39. package/lib-es/bridge/getTransactionStatus.js.map +1 -1
  40. package/lib-es/bridge/onboard.d.ts.map +1 -1
  41. package/lib-es/bridge/onboard.js +5 -2
  42. package/lib-es/bridge/onboard.js.map +1 -1
  43. package/lib-es/bridge/serialization.d.ts.map +1 -1
  44. package/lib-es/bridge/serialization.js +7 -1
  45. package/lib-es/bridge/serialization.js.map +1 -1
  46. package/lib-es/bridge/sync.d.ts.map +1 -1
  47. package/lib-es/bridge/sync.js +19 -14
  48. package/lib-es/bridge/sync.js.map +1 -1
  49. package/lib-es/network/gateway.d.ts +20 -3
  50. package/lib-es/network/gateway.d.ts.map +1 -1
  51. package/lib-es/network/gateway.js +70 -15
  52. package/lib-es/network/gateway.js.map +1 -1
  53. package/lib-es/test/cantonTestUtils.d.ts +3 -1
  54. package/lib-es/test/cantonTestUtils.d.ts.map +1 -1
  55. package/lib-es/test/cantonTestUtils.js +1 -1
  56. package/lib-es/test/cantonTestUtils.js.map +1 -1
  57. package/lib-es/types/bridge.d.ts +6 -0
  58. package/lib-es/types/bridge.d.ts.map +1 -1
  59. package/lib-es/types/errors.d.ts +3 -0
  60. package/lib-es/types/errors.d.ts.map +1 -1
  61. package/lib-es/types/errors.js +1 -0
  62. package/lib-es/types/errors.js.map +1 -1
  63. package/package.json +10 -10
  64. package/src/api/getBalance.integ.test.ts +1 -1
  65. package/src/api/lastBlock.integ.test.ts +1 -1
  66. package/src/api/listOperations.integ.test.ts +1 -1
  67. package/src/bridge/acceptOffer.test.ts +43 -4
  68. package/src/bridge/acceptOffer.ts +9 -1
  69. package/src/bridge/getTransactionStatus.test.ts +95 -3
  70. package/src/bridge/getTransactionStatus.ts +61 -24
  71. package/src/bridge/onboard.integ.test.ts +85 -4
  72. package/src/bridge/onboard.test.ts +107 -1
  73. package/src/bridge/onboard.ts +6 -1
  74. package/src/bridge/prepareTransaction.test.ts +1 -1
  75. package/src/bridge/serialization.ts +7 -1
  76. package/src/bridge/sync.integ.test.ts +1 -1
  77. package/src/bridge/sync.test.ts +156 -1
  78. package/src/bridge/sync.ts +22 -15
  79. package/src/network/gateway.integ.test.ts +24 -1
  80. package/src/network/gateway.test.ts +64 -2
  81. package/src/network/gateway.ts +98 -26
  82. package/src/test/cantonTestUtils.ts +1 -1
  83. package/src/types/bridge.ts +6 -0
  84. package/src/types/errors.ts +2 -0
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/types/errors.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,eAAe;;EAA4C,CAAC;AAEzE,eAAO,MAAM,oBAAoB;;EAAiD,CAAC;AACnF,eAAO,MAAM,mBAAmB;;EAAgD,CAAC"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/types/errors.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,eAAe;;EAA4C,CAAC;AAEzE,eAAO,MAAM,oBAAoB;;EAAiD,CAAC;AACnF,eAAO,MAAM,mBAAmB;;EAAgD,CAAC;AAEjF,eAAO,MAAM,mBAAmB;;EAAgD,CAAC"}
@@ -2,4 +2,5 @@ import { createCustomErrorClass } from "@ledgerhq/errors";
2
2
  export const SimulationError = createCustomErrorClass("SimulationError");
3
3
  export const TooManyUtxosCritical = createCustomErrorClass("TooManyUtxosCritical");
4
4
  export const TooManyUtxosWarning = createCustomErrorClass("TooManyUtxosWarning");
5
+ export const TopologyChangeError = createCustomErrorClass("TopologyChangeError");
5
6
  //# sourceMappingURL=errors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/types/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,MAAM,CAAC,MAAM,eAAe,GAAG,sBAAsB,CAAC,iBAAiB,CAAC,CAAC;AAEzE,MAAM,CAAC,MAAM,oBAAoB,GAAG,sBAAsB,CAAC,sBAAsB,CAAC,CAAC;AACnF,MAAM,CAAC,MAAM,mBAAmB,GAAG,sBAAsB,CAAC,qBAAqB,CAAC,CAAC"}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/types/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,MAAM,CAAC,MAAM,eAAe,GAAG,sBAAsB,CAAC,iBAAiB,CAAC,CAAC;AAEzE,MAAM,CAAC,MAAM,oBAAoB,GAAG,sBAAsB,CAAC,sBAAsB,CAAC,CAAC;AACnF,MAAM,CAAC,MAAM,mBAAmB,GAAG,sBAAsB,CAAC,qBAAqB,CAAC,CAAC;AAEjF,MAAM,CAAC,MAAM,mBAAmB,GAAG,sBAAsB,CAAC,qBAAqB,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ledgerhq/coin-canton",
3
- "version": "0.11.0",
3
+ "version": "0.12.0-nightly.20251210100832",
4
4
  "description": "Canton coin integration",
5
5
  "keywords": [
6
6
  "Ledger",
@@ -101,15 +101,15 @@
101
101
  "bignumber.js": "^9.1.2",
102
102
  "invariant": "^2.2.4",
103
103
  "protobufjs": "7.5.4",
104
- "rxjs": "^7.8.1",
105
- "@ledgerhq/coin-framework": "^6.10.0",
106
- "@ledgerhq/cryptoassets": "^13.34.0",
107
- "@ledgerhq/devices": "8.7.0",
108
- "@ledgerhq/errors": "^6.27.0",
109
- "@ledgerhq/live-env": "^2.22.0",
110
- "@ledgerhq/live-network": "^2.1.2",
104
+ "rxjs": "7.8.2",
105
+ "@ledgerhq/coin-framework": "^6.11.0-nightly.20251210100832",
106
+ "@ledgerhq/cryptoassets": "^13.35.0-nightly.20251210100832",
107
+ "@ledgerhq/devices": "8.8.0-nightly.20251210100832",
108
+ "@ledgerhq/live-env": "^2.23.0-nightly.20251210100832",
109
+ "@ledgerhq/live-network": "^2.1.3-nightly.20251210100832",
111
110
  "@ledgerhq/logs": "^6.13.0",
112
- "@ledgerhq/types-live": "^6.90.0"
111
+ "@ledgerhq/types-live": "^6.91.0-nightly.20251210100832",
112
+ "@ledgerhq/errors": "^6.28.0-nightly.20251210100832"
113
113
  },
114
114
  "devDependencies": {
115
115
  "@types/invariant": "^2.2.37",
@@ -121,7 +121,7 @@
121
121
  "ts-jest": "^29.1.1",
122
122
  "typescript": "^5.4.5",
123
123
  "@ledgerhq/disable-network-setup": "^0.1.0",
124
- "@ledgerhq/types-cryptoassets": "^7.30.0"
124
+ "@ledgerhq/types-cryptoassets": "^7.31.0-nightly.20251210100832"
125
125
  },
126
126
  "scripts": {
127
127
  "clean": "rimraf lib lib-es",
@@ -8,7 +8,7 @@ describe.skip("devnet", () => {
8
8
  api = createApi({
9
9
  nodeUrl: "https://wallet-validator-devnet-canton.ledger-test.com/v2",
10
10
  networkType: "devnet",
11
- gatewayUrl: "https://canton-gateway.api.live.ledger-test.com",
11
+ gatewayUrl: "https://canton-gateway-devnet.api.live.ledger-test.com",
12
12
  useGateway: true,
13
13
  nativeInstrumentId: "Amulet",
14
14
  });
@@ -8,7 +8,7 @@ describe.skip("devnet", () => {
8
8
  api = createApi({
9
9
  nodeUrl: "https://wallet-validator-devnet-canton.ledger-test.com/v2",
10
10
  networkType: "devnet",
11
- gatewayUrl: "https://canton-gateway.api.live.ledger-test.com",
11
+ gatewayUrl: "https://canton-gateway-devnet.api.live.ledger-test.com",
12
12
  useGateway: true,
13
13
  nativeInstrumentId: "Amulet",
14
14
  });
@@ -8,7 +8,7 @@ describe.skip("devnet", () => {
8
8
  api = createApi({
9
9
  nodeUrl: "https://wallet-validator-devnet-canton.ledger-test.com/v2",
10
10
  networkType: "devnet",
11
- gatewayUrl: "https://canton-gateway.api.live.ledger-test.com",
11
+ gatewayUrl: "https://canton-gateway-devnet.api.live.ledger-test.com",
12
12
  useGateway: true,
13
13
  nativeInstrumentId: "Amulet",
14
14
  });
@@ -1,17 +1,24 @@
1
1
  import { SignerContext } from "@ledgerhq/coin-framework/signer";
2
- import type { Account } from "@ledgerhq/types-live";
3
2
  import type { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
4
- import { buildTransferInstruction } from "./acceptOffer";
5
- import * as gateway from "../network/gateway";
3
+ import type { Account } from "@ledgerhq/types-live";
6
4
  import * as signTransactionModule from "../common-logic/transaction/sign";
7
- import type { CantonSigner, CantonSignature } from "../types/signer";
8
5
  import type { PrepareTransferResponse } from "../network/gateway";
6
+ import * as gateway from "../network/gateway";
7
+ import type { CantonAccount } from "../types";
8
+ import { TopologyChangeError } from "../types/errors";
9
+ import type { CantonSignature, CantonSigner } from "../types/signer";
10
+ import { buildTransferInstruction } from "./acceptOffer";
11
+ import * as getTransactionStatusModule from "./getTransactionStatus";
9
12
 
10
13
  jest.mock("../network/gateway");
11
14
  jest.mock("../common-logic/transaction/sign");
15
+ jest.mock("./getTransactionStatus");
12
16
 
13
17
  const mockedGateway = gateway as jest.Mocked<typeof gateway>;
14
18
  const mockedSignTransaction = signTransactionModule as jest.Mocked<typeof signTransactionModule>;
19
+ const mockedGetTransactionStatus = getTransactionStatusModule as jest.Mocked<
20
+ typeof getTransactionStatusModule
21
+ >;
15
22
 
16
23
  describe("acceptOffer", () => {
17
24
  const mockCurrency = {
@@ -95,6 +102,7 @@ describe("acceptOffer", () => {
95
102
  mockedGateway.prepareTransferInstruction.mockResolvedValue(mockPreparedTransaction);
96
103
  mockedSignTransaction.signTransaction.mockResolvedValue(mockSignature);
97
104
  mockedGateway.submitTransferInstruction.mockResolvedValue({ update_id: "test-update-id" });
105
+ mockedGetTransactionStatus.validateTopology.mockResolvedValue(null);
98
106
  });
99
107
 
100
108
  describe("buildTransferInstruction", () => {
@@ -296,5 +304,36 @@ describe("acceptOffer", () => {
296
304
  expect(mockedSignTransaction.signTransaction).toHaveBeenCalled();
297
305
  expect(mockedGateway.submitTransferInstruction).toHaveBeenCalled();
298
306
  });
307
+
308
+ it("should throw TopologyChangeError when validateTopology returns topology error", async () => {
309
+ // GIVEN
310
+ const topologyError = new TopologyChangeError("Topology change detected");
311
+ const cantonAccount = {
312
+ ...mockAccount,
313
+ cantonResources: {
314
+ publicKey: "test-public-key",
315
+ instrumentUtxoCounts: {},
316
+ },
317
+ } as unknown as CantonAccount;
318
+ mockedGetTransactionStatus.validateTopology.mockResolvedValue(topologyError);
319
+ const transferInstruction = buildTransferInstruction(mockSignerContext);
320
+
321
+ // WHEN & THEN
322
+ await expect(
323
+ transferInstruction(
324
+ mockCurrency,
325
+ mockDeviceId,
326
+ cantonAccount,
327
+ mockPartyId,
328
+ mockContractId,
329
+ "accept-transfer-instruction",
330
+ ),
331
+ ).rejects.toThrow(TopologyChangeError);
332
+
333
+ expect(mockedGetTransactionStatus.validateTopology).toHaveBeenCalledWith(cantonAccount);
334
+ expect(mockedGateway.prepareTransferInstruction).not.toHaveBeenCalled();
335
+ expect(mockedSignTransaction.signTransaction).not.toHaveBeenCalled();
336
+ expect(mockedGateway.submitTransferInstruction).not.toHaveBeenCalled();
337
+ });
299
338
  });
300
339
  });
@@ -1,9 +1,10 @@
1
1
  import { SignerContext } from "@ledgerhq/coin-framework/signer";
2
2
  import type { Account } from "@ledgerhq/types-live";
3
3
  import type { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
4
-
4
+ import { validateTopology } from "./getTransactionStatus";
5
5
  import { prepareTransferInstruction, submitTransferInstruction } from "../network/gateway";
6
6
  import { signTransaction } from "../common-logic/transaction/sign";
7
+ import { isCantonAccount } from "./serialization";
7
8
  import type { CantonSigner } from "../types";
8
9
 
9
10
  type TransferInstructionType =
@@ -22,6 +23,13 @@ export const buildTransferInstruction =
22
23
  type: TransferInstructionType,
23
24
  reason?: string,
24
25
  ) => {
26
+ if (isCantonAccount(account)) {
27
+ const topologyError = await validateTopology(account);
28
+ if (topologyError) {
29
+ throw topologyError;
30
+ }
31
+ }
32
+
25
33
  const preparedTransaction = await prepareTransferInstruction(currency, partyId, {
26
34
  type,
27
35
  contract_id: contractId,
@@ -1,18 +1,18 @@
1
1
  import {
2
2
  AmountRequired,
3
3
  FeeNotLoaded,
4
- FeeRequired,
5
4
  FeeTooHigh,
6
5
  InvalidAddress,
7
- InvalidAddressBecauseDestinationIsAlsoSource,
8
6
  NotEnoughBalanceBecauseDestinationNotCreated,
9
7
  NotEnoughSpendableBalance,
10
8
  RecipientRequired,
11
9
  } from "@ledgerhq/errors";
12
10
  import BigNumber from "bignumber.js";
13
11
  import coinConfig from "../config";
12
+ import * as gateway from "../network/gateway";
14
13
  import { createMockAccount } from "../test/fixtures";
15
14
  import { CantonAccount, TooManyUtxosCritical, TooManyUtxosWarning, Transaction } from "../types";
15
+ import { TopologyChangeError } from "../types/errors";
16
16
  import {
17
17
  getTransactionStatus,
18
18
  TO_MANY_UTXOS_CRITICAL_COUNT,
@@ -20,7 +20,9 @@ import {
20
20
  } from "./getTransactionStatus";
21
21
 
22
22
  jest.mock("../config", () => ({ getCoinConfig: jest.fn() }));
23
+ jest.mock("../network/gateway");
23
24
  const mockCoinConfig = jest.mocked(coinConfig);
25
+ const mockedGateway = gateway as jest.Mocked<typeof gateway>;
24
26
 
25
27
  describe("getTransactionStatus", () => {
26
28
  const mockAccount: CantonAccount = {
@@ -28,8 +30,11 @@ describe("getTransactionStatus", () => {
28
30
  balance: new BigNumber(1000),
29
31
  spendableBalance: new BigNumber(1000),
30
32
  freshAddress: "test::33333333333333333333333333333333333333333333333333333333333333333333",
33
+ xpub: "test-party-id",
31
34
  }),
32
35
  cantonResources: {
36
+ pendingTransferProposals: [],
37
+ publicKey: "test-public-key",
33
38
  instrumentUtxoCounts: {
34
39
  Amulet: 5,
35
40
  },
@@ -44,6 +49,7 @@ describe("getTransactionStatus", () => {
44
49
  status: { type: "active" },
45
50
  nativeInstrumentId: "Amulet",
46
51
  });
52
+ mockedGateway.isTopologyChangeRequiredCached.mockResolvedValue(false);
47
53
  });
48
54
 
49
55
  describe("fee validation", () => {
@@ -287,6 +293,7 @@ describe("getTransactionStatus", () => {
287
293
  const accountWithTooManyUtxos = {
288
294
  ...mockAccount,
289
295
  cantonResources: {
296
+ ...mockAccount.cantonResources,
290
297
  instrumentUtxoCounts: {
291
298
  Amulet: TO_MANY_UTXOS_CRITICAL_COUNT + 1,
292
299
  },
@@ -311,7 +318,7 @@ describe("getTransactionStatus", () => {
311
318
  const accountWithManyUtxos = {
312
319
  ...mockAccount,
313
320
  cantonResources: {
314
- pendingTransferProposals: [],
321
+ ...mockAccount.cantonResources,
315
322
  instrumentUtxoCounts: {
316
323
  Amulet: TO_MANY_UTXOS_WARNING_COUNT + 1,
317
324
  },
@@ -339,6 +346,7 @@ describe("getTransactionStatus", () => {
339
346
  const accountWithFewUtxos = {
340
347
  ...mockAccount,
341
348
  cantonResources: {
349
+ ...mockAccount.cantonResources,
342
350
  instrumentUtxoCounts: {
343
351
  Amulet: TO_MANY_UTXOS_WARNING_COUNT - 1,
344
352
  },
@@ -362,6 +370,7 @@ describe("getTransactionStatus", () => {
362
370
  const accountWithManyUtxos = {
363
371
  ...mockAccount,
364
372
  cantonResources: {
373
+ ...mockAccount.cantonResources,
365
374
  instrumentUtxoCounts: {
366
375
  Amulet: 25,
367
376
  },
@@ -382,4 +391,87 @@ describe("getTransactionStatus", () => {
382
391
  expect(result.errors.utxoCount).toBeUndefined();
383
392
  });
384
393
  });
394
+
395
+ describe("topology validation", () => {
396
+ it("should return TopologyChangeError when isTopologyChangeRequiredCached returns true", async () => {
397
+ // GIVEN
398
+ const accountWithPartyId: CantonAccount = {
399
+ ...mockAccount,
400
+ xpub: "test-party-id",
401
+ };
402
+ mockedGateway.isTopologyChangeRequiredCached.mockResolvedValue(true);
403
+
404
+ const transaction: Transaction = {
405
+ family: "canton",
406
+ amount: new BigNumber(100),
407
+ recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
408
+ fee: new BigNumber(10),
409
+ tokenId: "",
410
+ };
411
+
412
+ // WHEN
413
+ const result = await getTransactionStatus(accountWithPartyId, transaction);
414
+
415
+ // THEN
416
+ expect(result.errors.topologyChange).toBeInstanceOf(TopologyChangeError);
417
+ expect(mockedGateway.isTopologyChangeRequiredCached).toHaveBeenCalledWith(
418
+ accountWithPartyId.currency,
419
+ "test-public-key",
420
+ );
421
+ });
422
+
423
+ it("should not return topology error when isTopologyChangeRequiredCached returns false", async () => {
424
+ // GIVEN
425
+ const accountWithPartyId: CantonAccount = {
426
+ ...mockAccount,
427
+ xpub: "test-party-id",
428
+ };
429
+ mockedGateway.isTopologyChangeRequiredCached.mockResolvedValue(false);
430
+
431
+ const transaction: Transaction = {
432
+ family: "canton",
433
+ amount: new BigNumber(100),
434
+ recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
435
+ fee: new BigNumber(10),
436
+ tokenId: "",
437
+ };
438
+
439
+ // WHEN
440
+ const result = await getTransactionStatus(accountWithPartyId, transaction);
441
+
442
+ // THEN
443
+ expect(result.errors.topologyChange).toBeUndefined();
444
+ expect(mockedGateway.isTopologyChangeRequiredCached).toHaveBeenCalledWith(
445
+ accountWithPartyId.currency,
446
+ "test-public-key",
447
+ );
448
+ });
449
+
450
+ it("should not return topology error when isTopologyChangeRequiredCached throws an error", async () => {
451
+ // GIVEN
452
+ const accountWithPartyId: CantonAccount = {
453
+ ...mockAccount,
454
+ xpub: "test-party-id",
455
+ };
456
+ mockedGateway.isTopologyChangeRequiredCached.mockRejectedValue(new Error("Network error"));
457
+
458
+ const transaction: Transaction = {
459
+ family: "canton",
460
+ amount: new BigNumber(100),
461
+ recipient: "valid::11111111111111111111111111111111111111111111111111111111111111111111",
462
+ fee: new BigNumber(10),
463
+ tokenId: "",
464
+ };
465
+
466
+ // WHEN
467
+ const result = await getTransactionStatus(accountWithPartyId, transaction);
468
+
469
+ // THEN
470
+ expect(result.errors.topologyChange).toBeUndefined();
471
+ expect(mockedGateway.isTopologyChangeRequiredCached).toHaveBeenCalledWith(
472
+ accountWithPartyId.currency,
473
+ "test-public-key",
474
+ );
475
+ });
476
+ });
385
477
  });
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  AmountRequired,
3
3
  FeeNotLoaded,
4
- FeeRequired,
5
4
  FeeTooHigh,
6
5
  InvalidAddress,
7
6
  NotEnoughBalanceBecauseDestinationNotCreated,
@@ -21,6 +20,8 @@ import {
21
20
  import { isRecipientValid } from "../common-logic/utils";
22
21
  import coinConfig from "../config";
23
22
  import { getAbandonSeedAddress } from "@ledgerhq/cryptoassets/abandonseed";
23
+ import { isTopologyChangeRequiredCached } from "../network/gateway";
24
+ import { TopologyChangeError } from "../types/errors";
24
25
 
25
26
  export const TO_MANY_UTXOS_CRITICAL_COUNT = 24;
26
27
  export const TO_MANY_UTXOS_WARNING_COUNT = 10;
@@ -81,7 +82,15 @@ export const getTransactionStatus: AccountBridge<
81
82
  errors.amount = new AmountRequired();
82
83
  }
83
84
 
84
- validateUtxoCount(account, transaction, warnings);
85
+ const utxoWarning = validateUtxoCount(account, transaction);
86
+ if (utxoWarning) {
87
+ warnings.tooManyUtxos = utxoWarning;
88
+ }
89
+
90
+ const topologyError = await validateTopology(account);
91
+ if (topologyError) {
92
+ errors.topologyChange = topologyError;
93
+ }
85
94
 
86
95
  return {
87
96
  errors,
@@ -92,29 +101,57 @@ export const getTransactionStatus: AccountBridge<
92
101
  };
93
102
  };
94
103
 
95
- function validateUtxoCount(
96
- account: CantonAccount,
97
- transaction: Transaction,
98
- warnings: Record<string, Error>,
99
- ): void {
104
+ function validateUtxoCount(account: CantonAccount, transaction: Transaction): Error | null {
105
+ if (!account.cantonResources?.instrumentUtxoCounts) {
106
+ return null;
107
+ }
108
+
109
+ if (!transaction.recipient) {
110
+ return null;
111
+ }
112
+
113
+ if (!isRecipientValid(transaction.recipient) || account.xpub === transaction.recipient) {
114
+ return null;
115
+ }
116
+
100
117
  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
+ if (transaction.recipient.includes(abandonSeedAddress)) {
119
+ return null;
120
+ }
121
+
122
+ const { instrumentUtxoCounts } = account.cantonResources;
123
+ const instrumentUtxoCount = instrumentUtxoCounts[transaction.tokenId] || 0;
124
+
125
+ if (instrumentUtxoCount > TO_MANY_UTXOS_CRITICAL_COUNT) {
126
+ return new TooManyUtxosCritical();
127
+ }
128
+
129
+ if (instrumentUtxoCount > TO_MANY_UTXOS_WARNING_COUNT) {
130
+ return new TooManyUtxosWarning("families.canton.tooManyUtxos.warning");
131
+ }
132
+
133
+ return null;
134
+ }
135
+
136
+ export async function validateTopology(account: CantonAccount): Promise<Error | null> {
137
+ const publicKey = account.cantonResources?.publicKey;
138
+ if (!publicKey) {
139
+ return null;
140
+ }
141
+
142
+ try {
143
+ const isTopologyChangeRequired = await isTopologyChangeRequiredCached(
144
+ account.currency,
145
+ publicKey,
146
+ );
147
+
148
+ if (!isTopologyChangeRequired) {
149
+ return null;
118
150
  }
151
+
152
+ return new TopologyChangeError("Topology change detected. Re-onboarding required.");
153
+ } catch {
154
+ // If topology check fails, don't block the transaction
155
+ return null;
119
156
  }
120
157
  }
@@ -1,5 +1,5 @@
1
- import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
2
1
  import { firstValueFrom, toArray } from "rxjs";
2
+ import { getEnv, setEnv } from "@ledgerhq/live-env";
3
3
  import coinConfig from "../config";
4
4
  import { createMockSigner, generateMockKeyPair } from "../test/cantonTestUtils";
5
5
  import { createMockAccount, createMockCantonCurrency } from "../test/fixtures";
@@ -12,8 +12,12 @@ import {
12
12
  OnboardStatus,
13
13
  } from "../types/onboard";
14
14
  import { buildAuthorizePreapproval, buildOnboardAccount, isAccountOnboarded } from "./onboard";
15
+ import {
16
+ isTopologyChangeRequiredCached,
17
+ clearIsTopologyChangeRequiredCache,
18
+ } from "../network/gateway";
15
19
 
16
- describe.skip("onboard (devnet)", () => {
20
+ describe("onboard (devnet)", () => {
17
21
  const mockDeviceId = "test-device-id";
18
22
  const mockCurrency = createMockCantonCurrency();
19
23
  const mockAccount = createMockAccount();
@@ -27,7 +31,7 @@ describe.skip("onboard (devnet)", () => {
27
31
 
28
32
  beforeAll(() => {
29
33
  coinConfig.setCoinConfig(() => ({
30
- gatewayUrl: "https://canton-gateway.api.live.ledger-test.com",
34
+ gatewayUrl: "https://canton-gateway-devnet.api.live.ledger-test.com",
31
35
  useGateway: true,
32
36
  networkType: "devnet",
33
37
  nativeInstrumentId: "Amulet",
@@ -166,7 +170,7 @@ describe.skip("onboard (devnet)", () => {
166
170
  }, 30000);
167
171
  });
168
172
 
169
- describe("buildAuthorizePreapproval", () => {
173
+ describe.skip("buildAuthorizePreapproval", () => {
170
174
  it("should complete preapproval flow for onboarded account", async () => {
171
175
  // GIVEN
172
176
  const { mockSignerContext, onboardResult } = getOnboardedAccount();
@@ -199,4 +203,81 @@ describe.skip("onboard (devnet)", () => {
199
203
  expect(typeof finalResult.isApproved).toBe("boolean");
200
204
  }, 30000);
201
205
  });
206
+
207
+ describe("TopologyChangeError", () => {
208
+ it("should require topology change and complete re-onboarding when accessing account from different node", async () => {
209
+ // GIVEN
210
+ const originalNodeId = getEnv("CANTON_NODE_ID_OVERRIDE");
211
+ setEnv("CANTON_NODE_ID_OVERRIDE", "devnet");
212
+ const keyPair = generateMockKeyPair();
213
+ const mockAccount = createMockAccount({ xpub: keyPair.publicKeyHex });
214
+ const mockSigner = createMockSigner(keyPair);
215
+ const mockSignerContext = jest.fn().mockImplementation((_, callback) => {
216
+ return callback(mockSigner);
217
+ });
218
+
219
+ const onboardObservable = buildOnboardAccount(mockSignerContext);
220
+ const onboardValues = await firstValueFrom(
221
+ onboardObservable(mockCurrency, mockDeviceId, mockAccount).pipe(toArray()),
222
+ );
223
+ const onboardResult = onboardValues.find(
224
+ (value): value is CantonOnboardResult => "partyId" in value,
225
+ );
226
+
227
+ if (!onboardResult) {
228
+ throw new Error("Failed to onboard account");
229
+ }
230
+
231
+ const partyId = onboardResult.partyId;
232
+ expect(partyId).toBeDefined();
233
+
234
+ // Verify account is accessible on devnet node
235
+ const isTopologyChangeRequiredOnDevnet = await isTopologyChangeRequiredCached(
236
+ mockCurrency,
237
+ keyPair.publicKeyHex,
238
+ );
239
+ expect(isTopologyChangeRequiredOnDevnet).toBe(false);
240
+
241
+ // WHEN: Switch to different node
242
+ setEnv("CANTON_NODE_ID_OVERRIDE", "devnet-replicated");
243
+ clearIsTopologyChangeRequiredCache(mockCurrency, keyPair.publicKeyHex);
244
+
245
+ // THEN: Verify topology change is required
246
+ const isTopologyChangeRequiredOnReplicated = await isTopologyChangeRequiredCached(
247
+ mockCurrency,
248
+ keyPair.publicKeyHex,
249
+ );
250
+ expect(isTopologyChangeRequiredOnReplicated).toBe(true);
251
+
252
+ // AND: Verify re-onboarding proceeds through full onboarding flow
253
+ const reonboardObservable = buildOnboardAccount(mockSignerContext);
254
+ const reonboardValues = await firstValueFrom(
255
+ reonboardObservable(mockCurrency, mockDeviceId, mockAccount).pipe(toArray()),
256
+ );
257
+ const progressValues = reonboardValues.filter(
258
+ (value): value is CantonOnboardProgress => "status" in value && !("partyId" in value),
259
+ );
260
+ const resultValues = reonboardValues.filter(
261
+ (value): value is CantonOnboardResult => "partyId" in value,
262
+ );
263
+
264
+ // Check expected status progression
265
+ expect(progressValues.some(p => p.status === OnboardStatus.INIT)).toBe(true);
266
+ expect(progressValues.some(p => p.status === OnboardStatus.PREPARE)).toBe(true);
267
+ expect(progressValues.some(p => p.status === OnboardStatus.SIGN)).toBe(true);
268
+ expect(progressValues.some(p => p.status === OnboardStatus.SUBMIT)).toBe(true);
269
+
270
+ // Check final result
271
+ expect(resultValues.length).toBeGreaterThan(0);
272
+ const finalResult = resultValues[resultValues.length - 1];
273
+ expect(finalResult.partyId).toBeDefined();
274
+ expect(typeof finalResult.partyId).toBe("string");
275
+
276
+ if (originalNodeId) {
277
+ setEnv("CANTON_NODE_ID_OVERRIDE", originalNodeId);
278
+ } else {
279
+ setEnv("CANTON_NODE_ID_OVERRIDE", "");
280
+ }
281
+ }, 60000);
282
+ });
202
283
  });