@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,9 +1,17 @@
|
|
|
1
1
|
import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
|
|
2
|
-
import {
|
|
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
|
});
|
package/src/bridge/onboard.ts
CHANGED
|
@@ -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
|
|
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",
|
package/src/bridge/sync.test.ts
CHANGED
|
@@ -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
|
});
|
package/src/bridge/sync.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
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 {
|
|
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
|
});
|