@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,9 +1,17 @@
1
1
  import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
2
- import { isCantonCoinPreapproved } from "./onboard";
2
+ import { firstValueFrom, toArray } from "rxjs";
3
+ import { SignerContext } from "@ledgerhq/coin-framework/signer";
4
+ import { buildOnboardAccount, isCantonCoinPreapproved } from "./onboard";
3
5
  import * as gateway from "../network/gateway";
6
+ import * as signTransactionModule from "../common-logic/transaction/sign";
7
+ import { createMockAccount } from "../test/fixtures";
8
+ import type { CantonSigner, CantonSignature } from "../types";
9
+ import { OnboardStatus, CantonOnboardProgress, CantonOnboardResult } from "../types/onboard";
4
10
 
5
11
  jest.mock("../network/gateway");
12
+ jest.mock("../common-logic/transaction/sign");
6
13
  const mockedGateway = gateway as jest.Mocked<typeof gateway>;
14
+ const mockedSignTransaction = signTransactionModule as jest.Mocked<typeof signTransactionModule>;
7
15
 
8
16
  describe("onboard", () => {
9
17
  const mockPartyId = "test-party-id";
@@ -56,4 +64,102 @@ describe("onboard", () => {
56
64
  expect(mockedGateway.getTransferPreApproval).toHaveBeenCalledWith(mockCurrency, mockPartyId);
57
65
  });
58
66
  });
67
+
68
+ describe("buildOnboardAccount", () => {
69
+ const mockDeviceId = "test-device-id";
70
+ const mockPublicKey = "test-public-key";
71
+ const mockPartyId = "test-party-id";
72
+ const mockSignature: CantonSignature = {
73
+ signature: "test-signature",
74
+ };
75
+
76
+ const mockSigner: CantonSigner = {
77
+ getAddress: jest.fn().mockResolvedValue({
78
+ address: "test-address",
79
+ publicKey: mockPublicKey,
80
+ path: "44'/6767'/0'/0/0",
81
+ }),
82
+ signTransaction: jest.fn().mockResolvedValue(mockSignature),
83
+ } as unknown as CantonSigner;
84
+
85
+ const mockSignerContext: SignerContext<CantonSigner> = jest.fn(
86
+ async (deviceId: string, callback: (signer: CantonSigner) => Promise<CantonSignature>) => {
87
+ return callback(mockSigner);
88
+ },
89
+ ) as unknown as SignerContext<CantonSigner>;
90
+
91
+ beforeEach(() => {
92
+ jest.clearAllMocks();
93
+ });
94
+
95
+ it("should skip submission when account is onboarded on network but has no local xpub", async () => {
96
+ // GIVEN
97
+ const account = createMockAccount({ xpub: undefined });
98
+ mockedGateway.getPartyByPubKey.mockResolvedValue({ party_id: mockPartyId });
99
+
100
+ const onboardObservable = buildOnboardAccount(mockSignerContext);
101
+ const values = await firstValueFrom(
102
+ onboardObservable(mockCurrency, mockDeviceId, account).pipe(toArray()),
103
+ );
104
+
105
+ // THEN
106
+ const result = values.find((v): v is CantonOnboardResult => "partyId" in v);
107
+ expect(result).toBeDefined();
108
+ expect(result?.partyId).toBe(mockPartyId);
109
+ expect(result?.account.xpub).toBe(mockPartyId);
110
+
111
+ // Should NOT call prepareOnboarding or submitOnboarding
112
+ expect(mockedGateway.prepareOnboarding).not.toHaveBeenCalled();
113
+ expect(mockedGateway.submitOnboarding).not.toHaveBeenCalled();
114
+ expect(mockedSignTransaction.signTransaction).not.toHaveBeenCalled();
115
+ });
116
+
117
+ it("should proceed with submission when account has xpub (re-onboarding scenario)", async () => {
118
+ // GIVEN - account already has xpub (re-onboarding)
119
+ const existingPartyId = "existing-party-id";
120
+ const account = createMockAccount({ xpub: existingPartyId });
121
+ const newPartyId = "new-party-id";
122
+
123
+ mockedGateway.getPartyByPubKey.mockResolvedValue({ party_id: existingPartyId });
124
+ mockedGateway.prepareOnboarding.mockResolvedValue({
125
+ party_id: newPartyId,
126
+ transactions: {},
127
+ });
128
+ mockedGateway.submitOnboarding.mockResolvedValue({
129
+ party: {
130
+ party_id: newPartyId,
131
+ public_key: mockPublicKey,
132
+ },
133
+ });
134
+ mockedSignTransaction.signTransaction.mockResolvedValue(mockSignature);
135
+
136
+ const onboardObservable = buildOnboardAccount(mockSignerContext);
137
+ const values = await firstValueFrom(
138
+ onboardObservable(mockCurrency, mockDeviceId, account).pipe(toArray()),
139
+ );
140
+
141
+ // THEN - should proceed through full onboarding flow
142
+ const statuses = values
143
+ .filter((v): v is CantonOnboardProgress => "status" in v)
144
+ .map(v => v.status);
145
+ expect(statuses).toContain(OnboardStatus.PREPARE);
146
+ expect(statuses).toContain(OnboardStatus.SIGN);
147
+ expect(statuses).toContain(OnboardStatus.SUBMIT);
148
+
149
+ // Should call prepareOnboarding and submitOnboarding
150
+ expect(mockedGateway.prepareOnboarding).toHaveBeenCalledWith(mockCurrency, mockPublicKey);
151
+ expect(mockedGateway.submitOnboarding).toHaveBeenCalled();
152
+ expect(mockedSignTransaction.signTransaction).toHaveBeenCalled();
153
+
154
+ // Should clear topology change cache
155
+ expect(mockedGateway.clearIsTopologyChangeRequiredCache).toHaveBeenCalledWith(
156
+ mockCurrency,
157
+ mockPublicKey,
158
+ );
159
+
160
+ const result = values.find((v): v is CantonOnboardResult => "partyId" in v);
161
+ expect(result).toBeDefined();
162
+ expect(result?.partyId).toBe(newPartyId);
163
+ });
164
+ });
59
165
  });
@@ -16,6 +16,7 @@ import {
16
16
  preparePreApprovalTransaction,
17
17
  submitPreApprovalTransaction,
18
18
  getTransferPreApproval,
19
+ clearIsTopologyChangeRequiredCache,
19
20
  } from "../network/gateway";
20
21
  import { signTransaction } from "../common-logic/transaction/sign";
21
22
  import {
@@ -90,7 +91,9 @@ export const buildOnboardAccount =
90
91
 
91
92
  let { partyId } = await isAccountOnboarded(currency, publicKey);
92
93
 
93
- if (partyId) {
94
+ // Skip submission only if account is onboarded on network but has no local xpub.
95
+ // For re-onboarding (account has xpub), always proceed to submit a new onboarding transaction.
96
+ if (partyId && !account.xpub) {
94
97
  const onboardedAccount = createOnboardedAccount(account, partyId, currency);
95
98
  o.next({ partyId, account: onboardedAccount }); // success
96
99
  return;
@@ -109,6 +112,8 @@ export const buildOnboardAccount =
109
112
 
110
113
  await submitOnboarding(currency, publicKey, preparedTransaction, signature);
111
114
 
115
+ clearIsTopologyChangeRequiredCache(currency, publicKey);
116
+
112
117
  const onboardedAccount = createOnboardedAccount(account, partyId, currency);
113
118
  o.next({ partyId, account: onboardedAccount }); // success
114
119
  }
@@ -16,7 +16,7 @@ describe("prepareTransaction", () => {
16
16
 
17
17
  beforeAll(async () => {
18
18
  coinConfig.setCoinConfig(() => ({
19
- gatewayUrl: "https://canton-gateway.api.live.ledger-test.com",
19
+ gatewayUrl: "https://canton-gateway-devnet.api.live.ledger-test.com",
20
20
  useGateway: true,
21
21
  networkType: "devnet",
22
22
  nativeInstrumentId: "Amulet",
@@ -15,17 +15,23 @@ function isCantonAccountRaw(accountRaw: AccountRaw): accountRaw is CantonAccount
15
15
  }
16
16
 
17
17
  function toResourcesRaw(r: CantonResources): CantonResourcesRaw {
18
- const { instrumentUtxoCounts, pendingTransferProposals } = r;
18
+ const { isOnboarded, instrumentUtxoCounts, pendingTransferProposals, publicKey, xpub } = r;
19
19
  return {
20
+ isOnboarded: isOnboarded,
20
21
  instrumentUtxoCounts,
21
22
  pendingTransferProposals,
23
+ ...(publicKey !== undefined && { publicKey }),
24
+ ...(xpub !== undefined && { xpub }),
22
25
  };
23
26
  }
24
27
 
25
28
  function fromResourcesRaw(r: CantonResourcesRaw): CantonResources {
26
29
  return {
30
+ isOnboarded: r.isOnboarded,
27
31
  instrumentUtxoCounts: r.instrumentUtxoCounts,
28
32
  pendingTransferProposals: r.pendingTransferProposals,
33
+ ...(r.publicKey !== undefined && { publicKey: r.publicKey }),
34
+ ...(r.xpub !== undefined && { xpub: r.xpub }),
29
35
  };
30
36
  }
31
37
 
@@ -47,7 +47,7 @@ const mockSignerContext = jest.fn().mockImplementation((deviceId, callback) => {
47
47
  describe.skip("sync (devnet)", () => {
48
48
  beforeAll(async () => {
49
49
  coinConfig.setCoinConfig(() => ({
50
- gatewayUrl: "https://canton-gateway.api.live.ledger-test.com",
50
+ gatewayUrl: "https://canton-gateway-devnet.api.live.ledger-test.com",
51
51
  useGateway: true,
52
52
  networkType: "devnet",
53
53
  nativeInstrumentId: "Amulet",
@@ -1,5 +1,5 @@
1
1
  import { makeGetAccountShape } from "./sync";
2
- import { OperationInfo } from "../network/gateway";
2
+ import { OperationInfo, type InstrumentBalance } from "../network/gateway";
3
3
  import * as gateway from "../network/gateway";
4
4
  import * as onboard from "./onboard";
5
5
  import * as config from "../config";
@@ -7,6 +7,8 @@ import resolver from "../signer";
7
7
  import { AccountShapeInfo } from "@ledgerhq/coin-framework/bridge/jsHelpers";
8
8
  import { Account } from "@ledgerhq/types-live";
9
9
  import BigNumber from "bignumber.js";
10
+ import { CantonAccount } from "../types";
11
+ import { createMockCantonCurrency } from "../test/fixtures";
10
12
 
11
13
  jest.mock("../network/gateway");
12
14
  jest.mock("../signer");
@@ -25,6 +27,79 @@ const sampleCurrency = {
25
27
  id: "testcoin",
26
28
  };
27
29
 
30
+ const createMockInstrumentBalances = (): InstrumentBalance[] => [
31
+ {
32
+ instrument_id: "Native",
33
+ amount: "1000",
34
+ locked: false,
35
+ utxo_count: 1,
36
+ admin_id: "admin1",
37
+ },
38
+ ];
39
+
40
+ const createMockOperationView = (): OperationInfo =>
41
+ ({
42
+ transaction_hash: "tx-test",
43
+ uid: "uid-test",
44
+ type: "Send",
45
+ status: "Success",
46
+ fee: {
47
+ value: "5",
48
+ asset: {
49
+ type: "native",
50
+ issuer: null,
51
+ },
52
+ details: {
53
+ type: "fee",
54
+ },
55
+ },
56
+ transfers: [
57
+ {
58
+ address: "party123",
59
+ type: "Send",
60
+ value: "100",
61
+ asset: "Native",
62
+ details: {
63
+ operationType: "transfer",
64
+ metadata: {
65
+ reason: "test transfer",
66
+ },
67
+ },
68
+ },
69
+ ],
70
+ transaction_timestamp: new Date().toISOString(),
71
+ senders: ["party123"],
72
+ recipients: ["party456"],
73
+ block: {
74
+ height: 1,
75
+ time: new Date().toISOString(),
76
+ hash: "blockhash1",
77
+ },
78
+ asset: {
79
+ type: "native",
80
+ issuer: null,
81
+ },
82
+ details: {
83
+ operationType: "transfer",
84
+ },
85
+ }) as OperationInfo;
86
+
87
+ const createMockCantonAccountShapeInfo = (
88
+ overrides: Partial<AccountShapeInfo<CantonAccount>> = {},
89
+ ): AccountShapeInfo<CantonAccount> => {
90
+ const currency = createMockCantonCurrency();
91
+ return {
92
+ address: "addr1",
93
+ currency,
94
+ derivationMode: "",
95
+ derivationPath: "44'/0'/0'/0/0",
96
+ deviceId: "fakeDevice",
97
+ index: 0,
98
+ initialAccount: undefined,
99
+ ...overrides,
100
+ };
101
+ };
102
+
28
103
  describe("makeGetAccountShape", () => {
29
104
  const fakeSignerContext = {} as any;
30
105
 
@@ -335,4 +410,84 @@ describe("makeGetAccountShape", () => {
335
410
  expect(shape.operations[0].type).toBe("TRANSFER_WITHDRAWN");
336
411
  expect(shape.operations[0].value).toEqual(BigNumber(50)); // transfer value only, fees not added for TRANSFER_WITHDRAWN
337
412
  });
413
+
414
+ it("should sync without device when account has xpub but no publicKey", async () => {
415
+ mockedGetBalance.mockResolvedValue(createMockInstrumentBalances());
416
+ mockedGetOperations.mockResolvedValue({
417
+ operations: [createMockOperationView()],
418
+ });
419
+
420
+ const infoWithXpub = createMockCantonAccountShapeInfo({
421
+ deviceId: undefined, // No device
422
+ initialAccount: {
423
+ xpub: "test-party-id",
424
+ cantonResources: {
425
+ publicKey: undefined, // Missing publicKey
426
+ instrumentUtxoCounts: {},
427
+ pendingTransferProposals: [],
428
+ },
429
+ } as unknown as CantonAccount,
430
+ });
431
+
432
+ const getAccountShape = makeGetAccountShape(fakeSignerContext);
433
+ const shape = await getAccountShape(infoWithXpub, { paginationConfig: {} });
434
+
435
+ expect(shape).toHaveProperty("id");
436
+ expect(shape.xpub).toBe("test-party-id");
437
+ // Should not call getAddress since we have xpub
438
+ expect(mockedResolver).not.toHaveBeenCalled();
439
+ });
440
+
441
+ it("should sync without device when account has publicKey but no xpub", async () => {
442
+ mockedGetBalance.mockResolvedValue([]); // Empty balances since no xpub
443
+ mockedGetOperations.mockResolvedValue({
444
+ operations: [],
445
+ });
446
+
447
+ const infoWithPublicKey = createMockCantonAccountShapeInfo({
448
+ deviceId: undefined, // No device
449
+ initialAccount: {
450
+ xpub: "", // Missing xpub
451
+ cantonResources: {
452
+ publicKey: "test-public-key",
453
+ instrumentUtxoCounts: {},
454
+ pendingTransferProposals: [],
455
+ },
456
+ } as unknown as CantonAccount,
457
+ });
458
+
459
+ const getAccountShape = makeGetAccountShape(fakeSignerContext);
460
+ const shape = await getAccountShape(infoWithPublicKey, { paginationConfig: {} });
461
+
462
+ expect(shape).toHaveProperty("id");
463
+ // Should not call getAddress since we have publicKey (even though xpub is missing)
464
+ expect(mockedResolver).not.toHaveBeenCalled();
465
+ });
466
+
467
+ it("should sync without device when account has both xpub and publicKey", async () => {
468
+ mockedGetBalance.mockResolvedValue(createMockInstrumentBalances());
469
+ mockedGetOperations.mockResolvedValue({
470
+ operations: [createMockOperationView()],
471
+ });
472
+
473
+ const infoWithBoth = createMockCantonAccountShapeInfo({
474
+ deviceId: undefined, // No device
475
+ initialAccount: {
476
+ xpub: "test-party-id",
477
+ cantonResources: {
478
+ publicKey: "test-public-key",
479
+ instrumentUtxoCounts: {},
480
+ pendingTransferProposals: [],
481
+ },
482
+ } as unknown as CantonAccount,
483
+ });
484
+
485
+ const getAccountShape = makeGetAccountShape(fakeSignerContext);
486
+ const shape = await getAccountShape(infoWithBoth, { paginationConfig: {} });
487
+
488
+ expect(shape).toHaveProperty("id");
489
+ expect(shape.xpub).toBe("test-party-id");
490
+ // Should not call getAddress since we have both values
491
+ expect(mockedResolver).not.toHaveBeenCalled();
492
+ });
338
493
  });
@@ -13,7 +13,7 @@ import {
13
13
  import { getBalance, type CantonBalance } from "../common-logic/account/getBalance";
14
14
  import coinConfig from "../config";
15
15
  import resolver from "../signer";
16
- import { CantonAccount, CantonSigner } from "../types";
16
+ import { CantonAccount, CantonResources, CantonSigner } from "../types";
17
17
  import { isAccountOnboarded } from "./onboard";
18
18
  import { isCantonAccountEmpty } from "../helpers";
19
19
 
@@ -93,20 +93,25 @@ export function makeGetAccountShape(
93
93
  return async info => {
94
94
  const { address, currency, derivationMode, derivationPath, initialAccount } = info;
95
95
 
96
- let xpubOrAddress = initialAccount?.xpub || "";
96
+ let isOnboarded = initialAccount?.cantonResources?.isOnboarded ?? false;
97
+ let xpubOrAddress = (initialAccount?.xpub || initialAccount?.cantonResources?.xpub) ?? "";
98
+ let publicKey: string | undefined = initialAccount?.cantonResources?.publicKey;
97
99
 
98
- if (!xpubOrAddress) {
100
+ if (!xpubOrAddress && !publicKey) {
99
101
  const getAddress = resolver(signerContext);
100
- const { publicKey } = await getAddress(info.deviceId || "", {
102
+ const addressResult = await getAddress(info.deviceId ?? "", {
101
103
  path: derivationPath,
102
104
  currency: currency,
103
105
  derivationMode: derivationMode,
104
106
  verify: false,
105
107
  });
108
+ publicKey = addressResult.publicKey;
106
109
 
107
- const { isOnboarded, partyId } = await isAccountOnboarded(currency, publicKey);
108
- if (isOnboarded && partyId) {
109
- xpubOrAddress = partyId;
110
+ const result = await isAccountOnboarded(currency, publicKey);
111
+ isOnboarded = result.isOnboarded;
112
+
113
+ if (isOnboarded && result.partyId) {
114
+ xpubOrAddress = result.partyId;
110
115
  }
111
116
  }
112
117
 
@@ -161,14 +166,19 @@ export function makeGetAccountShape(
161
166
  operations = mergeOps(oldOperations, newOperations);
162
167
  }
163
168
 
169
+ const cantonResources: CantonResources = {
170
+ isOnboarded,
171
+ instrumentUtxoCounts,
172
+ pendingTransferProposals,
173
+ ...(publicKey && { publicKey }),
174
+ xpub: xpubOrAddress,
175
+ };
176
+
164
177
  const used = !isCantonAccountEmpty({
165
178
  operationsCount: operations.length,
166
179
  balance: totalBalance,
167
180
  subAccounts: initialAccount?.subAccounts ?? [],
168
- cantonResources: {
169
- instrumentUtxoCounts,
170
- pendingTransferProposals,
171
- },
181
+ cantonResources,
172
182
  });
173
183
 
174
184
  const blockHeight = await getLedgerEnd(currency);
@@ -192,10 +202,7 @@ export function makeGetAccountShape(
192
202
  spendableBalance,
193
203
  xpub: xpubOrAddress,
194
204
  used,
195
- cantonResources: {
196
- instrumentUtxoCounts,
197
- pendingTransferProposals,
198
- },
205
+ cantonResources,
199
206
  };
200
207
 
201
208
  return shape;
@@ -30,7 +30,7 @@ describe("gateway (devnet)", () => {
30
30
 
31
31
  beforeAll(async () => {
32
32
  coinConfig.setCoinConfig(() => ({
33
- gatewayUrl: "https://canton-gateway.api.live.ledger-test.com",
33
+ gatewayUrl: "https://canton-gateway-devnet.api.live.ledger-test.com",
34
34
  useGateway: true,
35
35
  nativeInstrumentId: "Amulet",
36
36
  networkType: "devnet",
@@ -126,6 +126,29 @@ describe("gateway (devnet)", () => {
126
126
  },
127
127
  30000,
128
128
  );
129
+
130
+ it("should handle PARTY_ALREADY_EXISTS error and return party_id and public_key", async () => {
131
+ // GIVEN
132
+ const { keyPair, partyId } = getOnboardedAccount();
133
+ const signature = keyPair.sign(prepareResponse?.transactions?.combined_hash || "");
134
+
135
+ // WHEN
136
+ const response = await submitOnboarding(
137
+ mockCurrency,
138
+ keyPair.publicKeyHex,
139
+ prepareResponse!,
140
+ {
141
+ signature,
142
+ },
143
+ );
144
+
145
+ // THEN
146
+ expect(response).toHaveProperty("party");
147
+ expect(response.party).toHaveProperty("party_id");
148
+ expect(response.party).toHaveProperty("public_key");
149
+ expect(response.party.party_id).toBe(partyId);
150
+ expect(response.party.public_key).toBe(keyPair.publicKeyHex);
151
+ }, 30000);
129
152
  });
130
153
 
131
154
  describe("getLedgerEnd", () => {
@@ -1,6 +1,15 @@
1
- import { getBalance, type GetBalanceResponse, type InstrumentBalance } from "./gateway";
1
+ import {
2
+ getBalance,
3
+ isPartyAlreadyExists,
4
+ submitOnboarding,
5
+ type GetBalanceResponse,
6
+ type InstrumentBalance,
7
+ type OnboardingPrepareResponse,
8
+ } from "./gateway";
2
9
  import type { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
3
10
  import coinConfig from "../config";
11
+ import { TopologyChangeError } from "../types/errors";
12
+ import { LedgerAPI4xx } from "@ledgerhq/errors";
4
13
 
5
14
  jest.mock("@ledgerhq/live-network", () => ({
6
15
  __esModule: true,
@@ -31,7 +40,7 @@ describe("getBalance", () => {
31
40
 
32
41
  beforeAll(() => {
33
42
  coinConfig.setCoinConfig(() => ({
34
- gatewayUrl: "https://canton-gateway.api.live.ledger-test.com",
43
+ gatewayUrl: "https://canton-gateway-devnet.api.live.ledger-test.com",
35
44
  useGateway: true,
36
45
  networkType: "devnet",
37
46
  nativeInstrumentId: "Amulet",
@@ -63,4 +72,57 @@ describe("getBalance", () => {
63
72
 
64
73
  expect(result).toEqual(mockResponse.balances);
65
74
  });
75
+
76
+ it("should handle PARTY_ALREADY_EXISTS error", async () => {
77
+ const mockPartyId = "test-party-id-123";
78
+ const mockPublicKey = "test-public-key-456";
79
+ const error = new LedgerAPI4xx("Party already exists", {
80
+ status: 409,
81
+ url: undefined,
82
+ method: "POST",
83
+ });
84
+ const mockPrepareResponse: OnboardingPrepareResponse = {
85
+ party_id: mockPartyId,
86
+ party_name: "Test Party",
87
+ public_key_fingerprint: "fingerprint",
88
+ transactions: {
89
+ namespace_transaction: {
90
+ serialized: "",
91
+ transaction: { operation: "", serial: 0, mapping: {} },
92
+ hash: "",
93
+ },
94
+ party_to_key_transaction: {
95
+ serialized: "",
96
+ transaction: { operation: "", serial: 0, mapping: {} },
97
+ hash: "",
98
+ },
99
+ party_to_participant_transaction: {
100
+ serialized: "",
101
+ transaction: { operation: "", serial: 0, mapping: {} },
102
+ hash: "",
103
+ },
104
+ combined_hash: "",
105
+ },
106
+ };
107
+
108
+ mockNetwork.mockRejectedValue(error);
109
+
110
+ const result = await submitOnboarding(mockCurrency, mockPublicKey, mockPrepareResponse, {
111
+ signature: "test-signature",
112
+ });
113
+
114
+ expect(isPartyAlreadyExists(error)).toBe(true);
115
+ expect(result.party).toEqual({ party_id: mockPartyId, public_key: mockPublicKey });
116
+ });
117
+
118
+ it("should handle PARTY_NOT_FOUND_BY_ID error", async () => {
119
+ const partyNotFoundError = new LedgerAPI4xx("Party not found", {
120
+ status: 400,
121
+ url: undefined,
122
+ method: "GET",
123
+ });
124
+ mockNetwork.mockRejectedValue(partyNotFoundError);
125
+
126
+ await expect(getBalance(mockCurrency, "test-party-id")).rejects.toThrow(TopologyChangeError);
127
+ });
66
128
  });