@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.
- package/CHANGELOG.md +23 -0
- package/README.md +1 -1
- package/lib/bridge/acceptOffer.d.ts.map +1 -1
- package/lib/bridge/acceptOffer.js +8 -0
- package/lib/bridge/acceptOffer.js.map +1 -1
- package/lib/bridge/getTransactionStatus.d.ts +1 -0
- package/lib/bridge/getTransactionStatus.d.ts.map +1 -1
- package/lib/bridge/getTransactionStatus.js +48 -17
- package/lib/bridge/getTransactionStatus.js.map +1 -1
- package/lib/bridge/onboard.d.ts.map +1 -1
- package/lib/bridge/onboard.js +4 -1
- package/lib/bridge/onboard.js.map +1 -1
- package/lib/bridge/serialization.d.ts.map +1 -1
- package/lib/bridge/serialization.js +7 -1
- package/lib/bridge/serialization.js.map +1 -1
- package/lib/bridge/sync.d.ts.map +1 -1
- package/lib/bridge/sync.js +19 -14
- package/lib/bridge/sync.js.map +1 -1
- package/lib/network/gateway.d.ts +20 -3
- package/lib/network/gateway.d.ts.map +1 -1
- package/lib/network/gateway.js +75 -16
- package/lib/network/gateway.js.map +1 -1
- package/lib/test/cantonTestUtils.d.ts +3 -1
- 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 +6 -0
- package/lib/types/bridge.d.ts.map +1 -1
- package/lib/types/errors.d.ts +3 -0
- package/lib/types/errors.d.ts.map +1 -1
- package/lib/types/errors.js +2 -1
- package/lib/types/errors.js.map +1 -1
- package/lib-es/bridge/acceptOffer.d.ts.map +1 -1
- package/lib-es/bridge/acceptOffer.js +8 -0
- package/lib-es/bridge/acceptOffer.js.map +1 -1
- package/lib-es/bridge/getTransactionStatus.d.ts +1 -0
- package/lib-es/bridge/getTransactionStatus.d.ts.map +1 -1
- package/lib-es/bridge/getTransactionStatus.js +47 -17
- 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 +5 -2
- 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 +7 -1
- 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 +19 -14
- package/lib-es/bridge/sync.js.map +1 -1
- package/lib-es/network/gateway.d.ts +20 -3
- package/lib-es/network/gateway.d.ts.map +1 -1
- package/lib-es/network/gateway.js +70 -15
- package/lib-es/network/gateway.js.map +1 -1
- package/lib-es/test/cantonTestUtils.d.ts +3 -1
- 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 +6 -0
- package/lib-es/types/bridge.d.ts.map +1 -1
- package/lib-es/types/errors.d.ts +3 -0
- package/lib-es/types/errors.d.ts.map +1 -1
- package/lib-es/types/errors.js +1 -0
- package/lib-es/types/errors.js.map +1 -1
- package/package.json +10 -10
- 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 +43 -4
- package/src/bridge/acceptOffer.ts +9 -1
- package/src/bridge/getTransactionStatus.test.ts +95 -3
- package/src/bridge/getTransactionStatus.ts +61 -24
- package/src/bridge/onboard.integ.test.ts +85 -4
- package/src/bridge/onboard.test.ts +107 -1
- package/src/bridge/onboard.ts +6 -1
- package/src/bridge/prepareTransaction.test.ts +1 -1
- package/src/bridge/serialization.ts +7 -1
- package/src/bridge/sync.integ.test.ts +1 -1
- package/src/bridge/sync.test.ts +156 -1
- package/src/bridge/sync.ts +22 -15
- package/src/network/gateway.integ.test.ts +24 -1
- package/src/network/gateway.test.ts +64 -2
- package/src/network/gateway.ts +98 -26
- package/src/test/cantonTestUtils.ts +1 -1
- package/src/types/bridge.ts +6 -0
- 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"}
|
package/lib-es/types/errors.js
CHANGED
|
@@ -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.
|
|
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": "
|
|
105
|
-
"@ledgerhq/coin-framework": "^6.
|
|
106
|
-
"@ledgerhq/cryptoassets": "^13.
|
|
107
|
-
"@ledgerhq/devices": "8.
|
|
108
|
-
"@ledgerhq/
|
|
109
|
-
"@ledgerhq/live-
|
|
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.
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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
|
});
|