@ledgerhq/coin-canton 0.11.0-nightly.20251205111238 → 0.11.0
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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +27 -10
- package/README.md +1 -1
- package/lib/bridge/acceptOffer.d.ts.map +1 -1
- package/lib/bridge/acceptOffer.js +0 -8
- package/lib/bridge/acceptOffer.js.map +1 -1
- package/lib/bridge/getTransactionStatus.d.ts +0 -1
- package/lib/bridge/getTransactionStatus.d.ts.map +1 -1
- package/lib/bridge/getTransactionStatus.js +17 -48
- package/lib/bridge/getTransactionStatus.js.map +1 -1
- package/lib/bridge/onboard.d.ts.map +1 -1
- package/lib/bridge/onboard.js +1 -4
- package/lib/bridge/onboard.js.map +1 -1
- package/lib/bridge/serialization.d.ts.map +1 -1
- package/lib/bridge/serialization.js +1 -3
- package/lib/bridge/serialization.js.map +1 -1
- package/lib/bridge/sync.d.ts.map +1 -1
- package/lib/bridge/sync.js +2 -6
- package/lib/bridge/sync.js.map +1 -1
- package/lib/network/gateway.d.ts +3 -20
- package/lib/network/gateway.d.ts.map +1 -1
- package/lib/network/gateway.js +16 -75
- package/lib/network/gateway.js.map +1 -1
- package/lib/test/cantonTestUtils.d.ts +1 -3
- package/lib/test/cantonTestUtils.d.ts.map +1 -1
- package/lib/test/cantonTestUtils.js +1 -1
- package/lib/test/cantonTestUtils.js.map +1 -1
- package/lib/types/bridge.d.ts +0 -2
- package/lib/types/bridge.d.ts.map +1 -1
- package/lib/types/errors.d.ts +0 -3
- package/lib/types/errors.d.ts.map +1 -1
- package/lib/types/errors.js +1 -2
- package/lib/types/errors.js.map +1 -1
- package/lib-es/bridge/acceptOffer.d.ts.map +1 -1
- package/lib-es/bridge/acceptOffer.js +0 -8
- package/lib-es/bridge/acceptOffer.js.map +1 -1
- package/lib-es/bridge/getTransactionStatus.d.ts +0 -1
- package/lib-es/bridge/getTransactionStatus.d.ts.map +1 -1
- package/lib-es/bridge/getTransactionStatus.js +17 -47
- package/lib-es/bridge/getTransactionStatus.js.map +1 -1
- package/lib-es/bridge/onboard.d.ts.map +1 -1
- package/lib-es/bridge/onboard.js +2 -5
- package/lib-es/bridge/onboard.js.map +1 -1
- package/lib-es/bridge/serialization.d.ts.map +1 -1
- package/lib-es/bridge/serialization.js +1 -3
- package/lib-es/bridge/serialization.js.map +1 -1
- package/lib-es/bridge/sync.d.ts.map +1 -1
- package/lib-es/bridge/sync.js +2 -6
- package/lib-es/bridge/sync.js.map +1 -1
- package/lib-es/network/gateway.d.ts +3 -20
- package/lib-es/network/gateway.d.ts.map +1 -1
- package/lib-es/network/gateway.js +15 -70
- package/lib-es/network/gateway.js.map +1 -1
- package/lib-es/test/cantonTestUtils.d.ts +1 -3
- package/lib-es/test/cantonTestUtils.d.ts.map +1 -1
- package/lib-es/test/cantonTestUtils.js +1 -1
- package/lib-es/test/cantonTestUtils.js.map +1 -1
- package/lib-es/types/bridge.d.ts +0 -2
- package/lib-es/types/bridge.d.ts.map +1 -1
- package/lib-es/types/errors.d.ts +0 -3
- package/lib-es/types/errors.d.ts.map +1 -1
- package/lib-es/types/errors.js +0 -1
- package/lib-es/types/errors.js.map +1 -1
- package/package.json +6 -6
- package/src/api/getBalance.integ.test.ts +1 -1
- package/src/api/lastBlock.integ.test.ts +1 -1
- package/src/api/listOperations.integ.test.ts +1 -1
- package/src/bridge/acceptOffer.test.ts +4 -43
- package/src/bridge/acceptOffer.ts +1 -9
- package/src/bridge/getTransactionStatus.test.ts +3 -95
- package/src/bridge/getTransactionStatus.ts +24 -61
- package/src/bridge/onboard.integ.test.ts +4 -85
- package/src/bridge/onboard.test.ts +1 -107
- package/src/bridge/onboard.ts +1 -6
- package/src/bridge/prepareTransaction.test.ts +1 -1
- package/src/bridge/serialization.ts +1 -3
- package/src/bridge/sync.integ.test.ts +1 -1
- package/src/bridge/sync.ts +2 -6
- package/src/network/gateway.integ.test.ts +1 -24
- package/src/network/gateway.test.ts +2 -64
- package/src/network/gateway.ts +26 -98
- package/src/test/cantonTestUtils.ts +1 -1
- package/src/types/bridge.ts +0 -2
- package/src/types/errors.ts +0 -2
|
@@ -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
|
|
11
|
+
gatewayUrl: "https://canton-gateway.api.live.ledger-test.com",
|
|
12
12
|
useGateway: true,
|
|
13
13
|
nativeInstrumentId: "Amulet",
|
|
14
14
|
});
|
|
@@ -1,24 +1,17 @@
|
|
|
1
1
|
import { SignerContext } from "@ledgerhq/coin-framework/signer";
|
|
2
|
-
import type { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
|
|
3
2
|
import type { Account } from "@ledgerhq/types-live";
|
|
3
|
+
import type { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
|
|
4
|
+
import { buildTransferInstruction } from "./acceptOffer";
|
|
5
|
+
import * as gateway from "../network/gateway";
|
|
4
6
|
import * as signTransactionModule from "../common-logic/transaction/sign";
|
|
7
|
+
import type { CantonSigner, CantonSignature } from "../types/signer";
|
|
5
8
|
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";
|
|
12
9
|
|
|
13
10
|
jest.mock("../network/gateway");
|
|
14
11
|
jest.mock("../common-logic/transaction/sign");
|
|
15
|
-
jest.mock("./getTransactionStatus");
|
|
16
12
|
|
|
17
13
|
const mockedGateway = gateway as jest.Mocked<typeof gateway>;
|
|
18
14
|
const mockedSignTransaction = signTransactionModule as jest.Mocked<typeof signTransactionModule>;
|
|
19
|
-
const mockedGetTransactionStatus = getTransactionStatusModule as jest.Mocked<
|
|
20
|
-
typeof getTransactionStatusModule
|
|
21
|
-
>;
|
|
22
15
|
|
|
23
16
|
describe("acceptOffer", () => {
|
|
24
17
|
const mockCurrency = {
|
|
@@ -102,7 +95,6 @@ describe("acceptOffer", () => {
|
|
|
102
95
|
mockedGateway.prepareTransferInstruction.mockResolvedValue(mockPreparedTransaction);
|
|
103
96
|
mockedSignTransaction.signTransaction.mockResolvedValue(mockSignature);
|
|
104
97
|
mockedGateway.submitTransferInstruction.mockResolvedValue({ update_id: "test-update-id" });
|
|
105
|
-
mockedGetTransactionStatus.validateTopology.mockResolvedValue(null);
|
|
106
98
|
});
|
|
107
99
|
|
|
108
100
|
describe("buildTransferInstruction", () => {
|
|
@@ -304,36 +296,5 @@ describe("acceptOffer", () => {
|
|
|
304
296
|
expect(mockedSignTransaction.signTransaction).toHaveBeenCalled();
|
|
305
297
|
expect(mockedGateway.submitTransferInstruction).toHaveBeenCalled();
|
|
306
298
|
});
|
|
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
|
-
});
|
|
338
299
|
});
|
|
339
300
|
});
|
|
@@ -1,10 +1,9 @@
|
|
|
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
|
+
|
|
5
5
|
import { prepareTransferInstruction, submitTransferInstruction } from "../network/gateway";
|
|
6
6
|
import { signTransaction } from "../common-logic/transaction/sign";
|
|
7
|
-
import { isCantonAccount } from "./serialization";
|
|
8
7
|
import type { CantonSigner } from "../types";
|
|
9
8
|
|
|
10
9
|
type TransferInstructionType =
|
|
@@ -23,13 +22,6 @@ export const buildTransferInstruction =
|
|
|
23
22
|
type: TransferInstructionType,
|
|
24
23
|
reason?: string,
|
|
25
24
|
) => {
|
|
26
|
-
if (isCantonAccount(account)) {
|
|
27
|
-
const topologyError = await validateTopology(account);
|
|
28
|
-
if (topologyError) {
|
|
29
|
-
throw topologyError;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
25
|
const preparedTransaction = await prepareTransferInstruction(currency, partyId, {
|
|
34
26
|
type,
|
|
35
27
|
contract_id: contractId,
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AmountRequired,
|
|
3
3
|
FeeNotLoaded,
|
|
4
|
+
FeeRequired,
|
|
4
5
|
FeeTooHigh,
|
|
5
6
|
InvalidAddress,
|
|
7
|
+
InvalidAddressBecauseDestinationIsAlsoSource,
|
|
6
8
|
NotEnoughBalanceBecauseDestinationNotCreated,
|
|
7
9
|
NotEnoughSpendableBalance,
|
|
8
10
|
RecipientRequired,
|
|
9
11
|
} from "@ledgerhq/errors";
|
|
10
12
|
import BigNumber from "bignumber.js";
|
|
11
13
|
import coinConfig from "../config";
|
|
12
|
-
import * as gateway from "../network/gateway";
|
|
13
14
|
import { createMockAccount } from "../test/fixtures";
|
|
14
15
|
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,9 +20,7 @@ import {
|
|
|
20
20
|
} from "./getTransactionStatus";
|
|
21
21
|
|
|
22
22
|
jest.mock("../config", () => ({ getCoinConfig: jest.fn() }));
|
|
23
|
-
jest.mock("../network/gateway");
|
|
24
23
|
const mockCoinConfig = jest.mocked(coinConfig);
|
|
25
|
-
const mockedGateway = gateway as jest.Mocked<typeof gateway>;
|
|
26
24
|
|
|
27
25
|
describe("getTransactionStatus", () => {
|
|
28
26
|
const mockAccount: CantonAccount = {
|
|
@@ -30,11 +28,8 @@ describe("getTransactionStatus", () => {
|
|
|
30
28
|
balance: new BigNumber(1000),
|
|
31
29
|
spendableBalance: new BigNumber(1000),
|
|
32
30
|
freshAddress: "test::33333333333333333333333333333333333333333333333333333333333333333333",
|
|
33
|
-
xpub: "test-party-id",
|
|
34
31
|
}),
|
|
35
32
|
cantonResources: {
|
|
36
|
-
pendingTransferProposals: [],
|
|
37
|
-
publicKey: "test-public-key",
|
|
38
33
|
instrumentUtxoCounts: {
|
|
39
34
|
Amulet: 5,
|
|
40
35
|
},
|
|
@@ -49,7 +44,6 @@ describe("getTransactionStatus", () => {
|
|
|
49
44
|
status: { type: "active" },
|
|
50
45
|
nativeInstrumentId: "Amulet",
|
|
51
46
|
});
|
|
52
|
-
mockedGateway.isTopologyChangeRequiredCached.mockResolvedValue(false);
|
|
53
47
|
});
|
|
54
48
|
|
|
55
49
|
describe("fee validation", () => {
|
|
@@ -293,7 +287,6 @@ describe("getTransactionStatus", () => {
|
|
|
293
287
|
const accountWithTooManyUtxos = {
|
|
294
288
|
...mockAccount,
|
|
295
289
|
cantonResources: {
|
|
296
|
-
...mockAccount.cantonResources,
|
|
297
290
|
instrumentUtxoCounts: {
|
|
298
291
|
Amulet: TO_MANY_UTXOS_CRITICAL_COUNT + 1,
|
|
299
292
|
},
|
|
@@ -318,7 +311,7 @@ describe("getTransactionStatus", () => {
|
|
|
318
311
|
const accountWithManyUtxos = {
|
|
319
312
|
...mockAccount,
|
|
320
313
|
cantonResources: {
|
|
321
|
-
|
|
314
|
+
pendingTransferProposals: [],
|
|
322
315
|
instrumentUtxoCounts: {
|
|
323
316
|
Amulet: TO_MANY_UTXOS_WARNING_COUNT + 1,
|
|
324
317
|
},
|
|
@@ -346,7 +339,6 @@ describe("getTransactionStatus", () => {
|
|
|
346
339
|
const accountWithFewUtxos = {
|
|
347
340
|
...mockAccount,
|
|
348
341
|
cantonResources: {
|
|
349
|
-
...mockAccount.cantonResources,
|
|
350
342
|
instrumentUtxoCounts: {
|
|
351
343
|
Amulet: TO_MANY_UTXOS_WARNING_COUNT - 1,
|
|
352
344
|
},
|
|
@@ -370,7 +362,6 @@ describe("getTransactionStatus", () => {
|
|
|
370
362
|
const accountWithManyUtxos = {
|
|
371
363
|
...mockAccount,
|
|
372
364
|
cantonResources: {
|
|
373
|
-
...mockAccount.cantonResources,
|
|
374
365
|
instrumentUtxoCounts: {
|
|
375
366
|
Amulet: 25,
|
|
376
367
|
},
|
|
@@ -391,87 +382,4 @@ describe("getTransactionStatus", () => {
|
|
|
391
382
|
expect(result.errors.utxoCount).toBeUndefined();
|
|
392
383
|
});
|
|
393
384
|
});
|
|
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
|
-
});
|
|
477
385
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AmountRequired,
|
|
3
3
|
FeeNotLoaded,
|
|
4
|
+
FeeRequired,
|
|
4
5
|
FeeTooHigh,
|
|
5
6
|
InvalidAddress,
|
|
6
7
|
NotEnoughBalanceBecauseDestinationNotCreated,
|
|
@@ -20,8 +21,6 @@ import {
|
|
|
20
21
|
import { isRecipientValid } from "../common-logic/utils";
|
|
21
22
|
import coinConfig from "../config";
|
|
22
23
|
import { getAbandonSeedAddress } from "@ledgerhq/cryptoassets/abandonseed";
|
|
23
|
-
import { isTopologyChangeRequiredCached } from "../network/gateway";
|
|
24
|
-
import { TopologyChangeError } from "../types/errors";
|
|
25
24
|
|
|
26
25
|
export const TO_MANY_UTXOS_CRITICAL_COUNT = 24;
|
|
27
26
|
export const TO_MANY_UTXOS_WARNING_COUNT = 10;
|
|
@@ -82,15 +81,7 @@ export const getTransactionStatus: AccountBridge<
|
|
|
82
81
|
errors.amount = new AmountRequired();
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
|
|
86
|
-
if (utxoWarning) {
|
|
87
|
-
warnings.tooManyUtxos = utxoWarning;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const topologyError = await validateTopology(account);
|
|
91
|
-
if (topologyError) {
|
|
92
|
-
errors.topologyChange = topologyError;
|
|
93
|
-
}
|
|
84
|
+
validateUtxoCount(account, transaction, warnings);
|
|
94
85
|
|
|
95
86
|
return {
|
|
96
87
|
errors,
|
|
@@ -101,57 +92,29 @@ export const getTransactionStatus: AccountBridge<
|
|
|
101
92
|
};
|
|
102
93
|
};
|
|
103
94
|
|
|
104
|
-
function validateUtxoCount(
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
95
|
+
function validateUtxoCount(
|
|
96
|
+
account: CantonAccount,
|
|
97
|
+
transaction: Transaction,
|
|
98
|
+
warnings: Record<string, Error>,
|
|
99
|
+
): void {
|
|
117
100
|
const abandonSeedAddress = getAbandonSeedAddress(account.currency.id);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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;
|
|
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");
|
|
150
118
|
}
|
|
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;
|
|
156
119
|
}
|
|
157
120
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
|
|
1
2
|
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,12 +12,8 @@ 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";
|
|
19
15
|
|
|
20
|
-
describe("onboard (devnet)", () => {
|
|
16
|
+
describe.skip("onboard (devnet)", () => {
|
|
21
17
|
const mockDeviceId = "test-device-id";
|
|
22
18
|
const mockCurrency = createMockCantonCurrency();
|
|
23
19
|
const mockAccount = createMockAccount();
|
|
@@ -31,7 +27,7 @@ describe("onboard (devnet)", () => {
|
|
|
31
27
|
|
|
32
28
|
beforeAll(() => {
|
|
33
29
|
coinConfig.setCoinConfig(() => ({
|
|
34
|
-
gatewayUrl: "https://canton-gateway
|
|
30
|
+
gatewayUrl: "https://canton-gateway.api.live.ledger-test.com",
|
|
35
31
|
useGateway: true,
|
|
36
32
|
networkType: "devnet",
|
|
37
33
|
nativeInstrumentId: "Amulet",
|
|
@@ -170,7 +166,7 @@ describe("onboard (devnet)", () => {
|
|
|
170
166
|
}, 30000);
|
|
171
167
|
});
|
|
172
168
|
|
|
173
|
-
describe
|
|
169
|
+
describe("buildAuthorizePreapproval", () => {
|
|
174
170
|
it("should complete preapproval flow for onboarded account", async () => {
|
|
175
171
|
// GIVEN
|
|
176
172
|
const { mockSignerContext, onboardResult } = getOnboardedAccount();
|
|
@@ -203,81 +199,4 @@ describe("onboard (devnet)", () => {
|
|
|
203
199
|
expect(typeof finalResult.isApproved).toBe("boolean");
|
|
204
200
|
}, 30000);
|
|
205
201
|
});
|
|
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
|
-
});
|
|
283
202
|
});
|
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
|
|
2
|
-
import {
|
|
3
|
-
import { SignerContext } from "@ledgerhq/coin-framework/signer";
|
|
4
|
-
import { buildOnboardAccount, isCantonCoinPreapproved } from "./onboard";
|
|
2
|
+
import { isCantonCoinPreapproved } from "./onboard";
|
|
5
3
|
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";
|
|
10
4
|
|
|
11
5
|
jest.mock("../network/gateway");
|
|
12
|
-
jest.mock("../common-logic/transaction/sign");
|
|
13
6
|
const mockedGateway = gateway as jest.Mocked<typeof gateway>;
|
|
14
|
-
const mockedSignTransaction = signTransactionModule as jest.Mocked<typeof signTransactionModule>;
|
|
15
7
|
|
|
16
8
|
describe("onboard", () => {
|
|
17
9
|
const mockPartyId = "test-party-id";
|
|
@@ -64,102 +56,4 @@ describe("onboard", () => {
|
|
|
64
56
|
expect(mockedGateway.getTransferPreApproval).toHaveBeenCalledWith(mockCurrency, mockPartyId);
|
|
65
57
|
});
|
|
66
58
|
});
|
|
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
|
-
});
|
|
165
59
|
});
|
package/src/bridge/onboard.ts
CHANGED
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
preparePreApprovalTransaction,
|
|
17
17
|
submitPreApprovalTransaction,
|
|
18
18
|
getTransferPreApproval,
|
|
19
|
-
clearIsTopologyChangeRequiredCache,
|
|
20
19
|
} from "../network/gateway";
|
|
21
20
|
import { signTransaction } from "../common-logic/transaction/sign";
|
|
22
21
|
import {
|
|
@@ -91,9 +90,7 @@ export const buildOnboardAccount =
|
|
|
91
90
|
|
|
92
91
|
let { partyId } = await isAccountOnboarded(currency, publicKey);
|
|
93
92
|
|
|
94
|
-
|
|
95
|
-
// For re-onboarding (account has xpub), always proceed to submit a new onboarding transaction.
|
|
96
|
-
if (partyId && !account.xpub) {
|
|
93
|
+
if (partyId) {
|
|
97
94
|
const onboardedAccount = createOnboardedAccount(account, partyId, currency);
|
|
98
95
|
o.next({ partyId, account: onboardedAccount }); // success
|
|
99
96
|
return;
|
|
@@ -112,8 +109,6 @@ export const buildOnboardAccount =
|
|
|
112
109
|
|
|
113
110
|
await submitOnboarding(currency, publicKey, preparedTransaction, signature);
|
|
114
111
|
|
|
115
|
-
clearIsTopologyChangeRequiredCache(currency, publicKey);
|
|
116
|
-
|
|
117
112
|
const onboardedAccount = createOnboardedAccount(account, partyId, currency);
|
|
118
113
|
o.next({ partyId, account: onboardedAccount }); // success
|
|
119
114
|
}
|
|
@@ -16,7 +16,7 @@ describe("prepareTransaction", () => {
|
|
|
16
16
|
|
|
17
17
|
beforeAll(async () => {
|
|
18
18
|
coinConfig.setCoinConfig(() => ({
|
|
19
|
-
gatewayUrl: "https://canton-gateway
|
|
19
|
+
gatewayUrl: "https://canton-gateway.api.live.ledger-test.com",
|
|
20
20
|
useGateway: true,
|
|
21
21
|
networkType: "devnet",
|
|
22
22
|
nativeInstrumentId: "Amulet",
|