@ledgerhq/coin-canton 0.9.0-nightly.1 → 0.9.0-nightly.2
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 +16 -0
- package/README.md +75 -0
- package/lib/bridge/deviceTransactionConfig.d.ts +1 -1
- package/lib/bridge/deviceTransactionConfig.d.ts.map +1 -1
- package/lib/bridge/deviceTransactionConfig.js +1 -1
- package/lib/bridge/deviceTransactionConfig.js.map +1 -1
- package/lib/bridge/onboard.d.ts.map +1 -1
- package/lib/bridge/onboard.js +48 -22
- package/lib/bridge/onboard.js.map +1 -1
- package/lib/bridge/signOperation.d.ts.map +1 -1
- package/lib/bridge/signOperation.js +10 -3
- package/lib/bridge/signOperation.js.map +1 -1
- package/lib/common-logic/index.d.ts +1 -0
- package/lib/common-logic/index.d.ts.map +1 -1
- package/lib/common-logic/index.js +3 -1
- package/lib/common-logic/index.js.map +1 -1
- package/lib/common-logic/transaction/sign.d.ts +8 -0
- package/lib/common-logic/transaction/sign.d.ts.map +1 -0
- package/lib/common-logic/transaction/sign.js +27 -0
- package/lib/common-logic/transaction/sign.js.map +1 -0
- package/lib/common-logic/transaction/split.d.ts +9 -0
- package/lib/common-logic/transaction/split.d.ts.map +1 -0
- package/lib/common-logic/transaction/split.js +119 -0
- package/lib/common-logic/transaction/split.js.map +1 -0
- package/lib/common-logic/utils.js +2 -2
- package/lib/common-logic/utils.js.map +1 -1
- package/lib/network/gateway.d.ts +1 -0
- package/lib/network/gateway.d.ts.map +1 -1
- package/lib/network/gateway.js +2 -7
- package/lib/network/gateway.js.map +1 -1
- package/lib/test/cantonTestUtils.d.ts +4 -9
- package/lib/test/cantonTestUtils.d.ts.map +1 -1
- package/lib/test/cantonTestUtils.js +736 -27
- package/lib/test/cantonTestUtils.js.map +1 -1
- package/lib/types/signer.d.ts +10 -1
- package/lib/types/signer.d.ts.map +1 -1
- package/lib/types/transaction-proto.json +1238 -0
- package/lib-es/bridge/deviceTransactionConfig.d.ts +1 -1
- package/lib-es/bridge/deviceTransactionConfig.d.ts.map +1 -1
- package/lib-es/bridge/deviceTransactionConfig.js +1 -1
- package/lib-es/bridge/deviceTransactionConfig.js.map +1 -1
- package/lib-es/bridge/onboard.d.ts.map +1 -1
- package/lib-es/bridge/onboard.js +49 -23
- package/lib-es/bridge/onboard.js.map +1 -1
- package/lib-es/bridge/signOperation.d.ts.map +1 -1
- package/lib-es/bridge/signOperation.js +10 -3
- package/lib-es/bridge/signOperation.js.map +1 -1
- package/lib-es/common-logic/index.d.ts +1 -0
- package/lib-es/common-logic/index.d.ts.map +1 -1
- package/lib-es/common-logic/index.js +1 -0
- package/lib-es/common-logic/index.js.map +1 -1
- package/lib-es/common-logic/transaction/sign.d.ts +8 -0
- package/lib-es/common-logic/transaction/sign.d.ts.map +1 -0
- package/lib-es/common-logic/transaction/sign.js +24 -0
- package/lib-es/common-logic/transaction/sign.js.map +1 -0
- package/lib-es/common-logic/transaction/split.d.ts +9 -0
- package/lib-es/common-logic/transaction/split.d.ts.map +1 -0
- package/lib-es/common-logic/transaction/split.js +83 -0
- package/lib-es/common-logic/transaction/split.js.map +1 -0
- package/lib-es/common-logic/utils.js +2 -2
- package/lib-es/common-logic/utils.js.map +1 -1
- package/lib-es/network/gateway.d.ts +1 -0
- package/lib-es/network/gateway.d.ts.map +1 -1
- package/lib-es/network/gateway.js +1 -8
- package/lib-es/network/gateway.js.map +1 -1
- package/lib-es/test/cantonTestUtils.d.ts +4 -9
- package/lib-es/test/cantonTestUtils.d.ts.map +1 -1
- package/lib-es/test/cantonTestUtils.js +697 -21
- package/lib-es/test/cantonTestUtils.js.map +1 -1
- package/lib-es/types/signer.d.ts +10 -1
- package/lib-es/types/signer.d.ts.map +1 -1
- package/lib-es/types/transaction-proto.json +1238 -0
- package/package.json +10 -7
- package/scripts/generate.js +261 -0
- package/src/bridge/deviceTransactionConfig.test.ts +14 -10
- package/src/bridge/deviceTransactionConfig.ts +2 -2
- package/src/bridge/getTransactionStatus.test.ts +19 -19
- package/src/bridge/onboard.integ.test.ts +0 -20
- package/src/bridge/onboard.ts +57 -33
- package/src/bridge/signOperation.test.ts +114 -0
- package/src/bridge/signOperation.ts +12 -5
- package/src/bridge/sync.integ.test.ts +157 -132
- package/src/common-logic/index.ts +1 -0
- package/src/common-logic/transaction/sign.test.ts +317 -0
- package/src/common-logic/transaction/sign.ts +33 -0
- package/src/common-logic/transaction/split.test.ts +50 -0
- package/src/common-logic/transaction/split.ts +101 -0
- package/src/common-logic/utils.test.ts +22 -30
- package/src/common-logic/utils.ts +2 -2
- package/src/network/gateway.integ.test.ts +2 -0
- package/src/network/gateway.ts +3 -8
- package/src/test/cantonTestUtils.ts +789 -24
- package/src/test/prepare-transfer-serialized.json +26 -0
- package/src/test/prepare-transfer.json +3298 -0
- package/src/types/signer.ts +17 -3
- package/src/types/transaction-proto.json +1238 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { OnboardingPrepareResponse, PrepareTransferResponse } from "../../network/gateway";
|
|
2
|
+
import { CantonPreparedTransaction, CantonSigner } from "../../types/signer";
|
|
3
|
+
import { signTransaction } from "./sign";
|
|
4
|
+
|
|
5
|
+
class MockCantonSigner implements CantonSigner {
|
|
6
|
+
async getAddress(path: string, display?: boolean) {
|
|
7
|
+
return {
|
|
8
|
+
publicKey: "mock-public-key",
|
|
9
|
+
address: "mock-address",
|
|
10
|
+
path,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async signTransaction(
|
|
15
|
+
path: string,
|
|
16
|
+
data: CantonPreparedTransaction | { transactions: string[] },
|
|
17
|
+
) {
|
|
18
|
+
if ("transactions" in data) {
|
|
19
|
+
return `untyped-signature-${data.transactions.length}`;
|
|
20
|
+
} else {
|
|
21
|
+
return `prepared-transaction-signature-${data.damlTransaction.length}-${data.nodes.length}`;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("signTransaction", () => {
|
|
27
|
+
const mockSigner = new MockCantonSigner();
|
|
28
|
+
const mockDerivationPath = "44'/6767'/0'/0'/0'";
|
|
29
|
+
|
|
30
|
+
it("should sign prepared transaction", async () => {
|
|
31
|
+
// GIVEN
|
|
32
|
+
const mockPrepareTransferResponse: PrepareTransferResponse = {
|
|
33
|
+
json: {
|
|
34
|
+
transaction: {
|
|
35
|
+
version: "2.1",
|
|
36
|
+
roots: ["0"],
|
|
37
|
+
nodes: [
|
|
38
|
+
{
|
|
39
|
+
nodeId: "0",
|
|
40
|
+
v1: {
|
|
41
|
+
create: {
|
|
42
|
+
lfVersion: "2.1",
|
|
43
|
+
contractId: "test-contract-id",
|
|
44
|
+
packageName: "test-package",
|
|
45
|
+
templateId: {
|
|
46
|
+
packageId: "test-package-id",
|
|
47
|
+
moduleName: "TestModule",
|
|
48
|
+
entityName: "TestEntity",
|
|
49
|
+
},
|
|
50
|
+
argument: {
|
|
51
|
+
record: {
|
|
52
|
+
recordId: {
|
|
53
|
+
packageId: "test-package-id",
|
|
54
|
+
moduleName: "TestModule",
|
|
55
|
+
entityName: "TestEntity",
|
|
56
|
+
},
|
|
57
|
+
fields: [],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
metadata: {
|
|
66
|
+
submitterInfo: {
|
|
67
|
+
actAs: ["test::party"],
|
|
68
|
+
commandId: "test-command-id",
|
|
69
|
+
},
|
|
70
|
+
synchronizerId: "test-synchronizer-id",
|
|
71
|
+
transactionUuid: "test-transaction-uuid",
|
|
72
|
+
submissionTime: "1234567890",
|
|
73
|
+
inputContracts: [],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
serialized: "serialized-transaction",
|
|
77
|
+
hash: "test-hash",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// WHEN
|
|
81
|
+
const result = await signTransaction(
|
|
82
|
+
mockSigner,
|
|
83
|
+
mockDerivationPath,
|
|
84
|
+
mockPrepareTransferResponse,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// THEN
|
|
88
|
+
expect(result).toBe("prepared-transaction-signature-10-1");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should sign untyped versioned message", async () => {
|
|
92
|
+
// GIVEN
|
|
93
|
+
const mockOnboardingPrepareResponse: OnboardingPrepareResponse = {
|
|
94
|
+
party_id: "test-party-id",
|
|
95
|
+
party_name: "test-party-name",
|
|
96
|
+
public_key_fingerprint: "test-fingerprint",
|
|
97
|
+
transactions: {
|
|
98
|
+
namespace_transaction: {
|
|
99
|
+
serialized: "namespace-transaction-data",
|
|
100
|
+
json: {},
|
|
101
|
+
hash: "namespace-hash",
|
|
102
|
+
},
|
|
103
|
+
party_to_key_transaction: {
|
|
104
|
+
serialized: "party-to-key-transaction-data",
|
|
105
|
+
json: {},
|
|
106
|
+
hash: "party-to-key-hash",
|
|
107
|
+
},
|
|
108
|
+
party_to_participant_transaction: {
|
|
109
|
+
serialized: "party-to-participant-transaction-data",
|
|
110
|
+
json: {},
|
|
111
|
+
hash: "party-to-participant-hash",
|
|
112
|
+
},
|
|
113
|
+
combined_hash: "combined-hash",
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// WHEN
|
|
118
|
+
const result = await signTransaction(
|
|
119
|
+
mockSigner,
|
|
120
|
+
mockDerivationPath,
|
|
121
|
+
mockOnboardingPrepareResponse,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// THEN
|
|
125
|
+
expect(result).toBe("untyped-signature-3");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should handle empty signature from signer", async () => {
|
|
129
|
+
// GIVEN
|
|
130
|
+
const mockSignerWithEmptySignature = {
|
|
131
|
+
...mockSigner,
|
|
132
|
+
signTransaction: jest.fn().mockResolvedValue(""),
|
|
133
|
+
} as unknown as CantonSigner;
|
|
134
|
+
|
|
135
|
+
const mockPrepareTransferResponse: PrepareTransferResponse = {
|
|
136
|
+
json: {
|
|
137
|
+
transaction: {
|
|
138
|
+
version: "2.1",
|
|
139
|
+
roots: ["0"],
|
|
140
|
+
nodes: [],
|
|
141
|
+
},
|
|
142
|
+
metadata: {
|
|
143
|
+
submitterInfo: {
|
|
144
|
+
actAs: ["test::party"],
|
|
145
|
+
commandId: "test-command-id",
|
|
146
|
+
},
|
|
147
|
+
synchronizerId: "test-synchronizer-id",
|
|
148
|
+
transactionUuid: "test-transaction-uuid",
|
|
149
|
+
submissionTime: "1234567890",
|
|
150
|
+
inputContracts: [],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
serialized: "serialized-transaction",
|
|
154
|
+
hash: "test-hash",
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// WHEN & THEN
|
|
158
|
+
await expect(
|
|
159
|
+
signTransaction(
|
|
160
|
+
mockSignerWithEmptySignature,
|
|
161
|
+
mockDerivationPath,
|
|
162
|
+
mockPrepareTransferResponse,
|
|
163
|
+
),
|
|
164
|
+
).rejects.toThrow("Device returned empty signature");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should handle signer errors", async () => {
|
|
168
|
+
// GIVEN
|
|
169
|
+
const mockSignerWithError = {
|
|
170
|
+
...mockSigner,
|
|
171
|
+
signTransaction: jest.fn().mockRejectedValue(new Error("Signer error")),
|
|
172
|
+
} as unknown as CantonSigner;
|
|
173
|
+
|
|
174
|
+
const mockPrepareTransferResponse: PrepareTransferResponse = {
|
|
175
|
+
json: {
|
|
176
|
+
transaction: {
|
|
177
|
+
version: "2.1",
|
|
178
|
+
roots: ["0"],
|
|
179
|
+
nodes: [],
|
|
180
|
+
},
|
|
181
|
+
metadata: {
|
|
182
|
+
submitterInfo: {
|
|
183
|
+
actAs: ["test::party"],
|
|
184
|
+
commandId: "test-command-id",
|
|
185
|
+
},
|
|
186
|
+
synchronizerId: "test-synchronizer-id",
|
|
187
|
+
transactionUuid: "test-transaction-uuid",
|
|
188
|
+
submissionTime: "1234567890",
|
|
189
|
+
inputContracts: [],
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
serialized: "serialized-transaction",
|
|
193
|
+
hash: "test-hash",
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// WHEN & THEN
|
|
197
|
+
await expect(
|
|
198
|
+
signTransaction(mockSignerWithError, mockDerivationPath, mockPrepareTransferResponse),
|
|
199
|
+
).rejects.toThrow("Signer error");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("should call signer with correct parameters for prepared transaction", async () => {
|
|
203
|
+
// GIVEN
|
|
204
|
+
const mockSignerSpy = jest.fn().mockResolvedValue("test-signature");
|
|
205
|
+
const mockSignerWithSpy = {
|
|
206
|
+
...mockSigner,
|
|
207
|
+
signTransaction: mockSignerSpy,
|
|
208
|
+
} as unknown as CantonSigner;
|
|
209
|
+
|
|
210
|
+
const mockPrepareTransferResponse: PrepareTransferResponse = {
|
|
211
|
+
json: {
|
|
212
|
+
transaction: {
|
|
213
|
+
version: "2.1",
|
|
214
|
+
roots: ["0"],
|
|
215
|
+
nodes: [
|
|
216
|
+
{
|
|
217
|
+
nodeId: "0",
|
|
218
|
+
v1: {
|
|
219
|
+
create: {
|
|
220
|
+
lfVersion: "2.1",
|
|
221
|
+
contractId: "test-contract-id",
|
|
222
|
+
packageName: "test-package",
|
|
223
|
+
templateId: {
|
|
224
|
+
packageId: "test-package-id",
|
|
225
|
+
moduleName: "TestModule",
|
|
226
|
+
entityName: "TestEntity",
|
|
227
|
+
},
|
|
228
|
+
argument: {
|
|
229
|
+
record: {
|
|
230
|
+
recordId: {
|
|
231
|
+
packageId: "test-package-id",
|
|
232
|
+
moduleName: "TestModule",
|
|
233
|
+
entityName: "TestEntity",
|
|
234
|
+
},
|
|
235
|
+
fields: [],
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
},
|
|
243
|
+
metadata: {
|
|
244
|
+
submitterInfo: {
|
|
245
|
+
actAs: ["test::party"],
|
|
246
|
+
commandId: "test-command-id",
|
|
247
|
+
},
|
|
248
|
+
synchronizerId: "test-synchronizer-id",
|
|
249
|
+
transactionUuid: "test-transaction-uuid",
|
|
250
|
+
submissionTime: "1234567890",
|
|
251
|
+
inputContracts: [],
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
serialized: "serialized-transaction",
|
|
255
|
+
hash: "test-hash",
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// WHEN
|
|
259
|
+
await signTransaction(mockSignerWithSpy, mockDerivationPath, mockPrepareTransferResponse);
|
|
260
|
+
|
|
261
|
+
// THEN
|
|
262
|
+
expect(mockSignerSpy).toHaveBeenCalledWith(
|
|
263
|
+
mockDerivationPath,
|
|
264
|
+
expect.objectContaining({
|
|
265
|
+
damlTransaction: expect.any(Uint8Array),
|
|
266
|
+
nodes: expect.any(Array),
|
|
267
|
+
metadata: expect.any(Uint8Array),
|
|
268
|
+
inputContracts: expect.any(Array),
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("should call signer with correct parameters for untyped versioned message", async () => {
|
|
274
|
+
// GIVEN
|
|
275
|
+
const mockSignerSpy = jest.fn().mockResolvedValue("test-signature");
|
|
276
|
+
const mockSignerWithSpy = {
|
|
277
|
+
...mockSigner,
|
|
278
|
+
signTransaction: mockSignerSpy,
|
|
279
|
+
} as unknown as CantonSigner;
|
|
280
|
+
|
|
281
|
+
const mockOnboardingPrepareResponse: OnboardingPrepareResponse = {
|
|
282
|
+
party_id: "test-party-id",
|
|
283
|
+
party_name: "test-party-name",
|
|
284
|
+
public_key_fingerprint: "test-fingerprint",
|
|
285
|
+
transactions: {
|
|
286
|
+
namespace_transaction: {
|
|
287
|
+
serialized: "namespace-transaction-data",
|
|
288
|
+
json: {},
|
|
289
|
+
hash: "namespace-hash",
|
|
290
|
+
},
|
|
291
|
+
party_to_key_transaction: {
|
|
292
|
+
serialized: "party-to-key-transaction-data",
|
|
293
|
+
json: {},
|
|
294
|
+
hash: "party-to-key-hash",
|
|
295
|
+
},
|
|
296
|
+
party_to_participant_transaction: {
|
|
297
|
+
serialized: "party-to-participant-transaction-data",
|
|
298
|
+
json: {},
|
|
299
|
+
hash: "party-to-participant-hash",
|
|
300
|
+
},
|
|
301
|
+
combined_hash: "combined-hash",
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// WHEN
|
|
306
|
+
await signTransaction(mockSignerWithSpy, mockDerivationPath, mockOnboardingPrepareResponse);
|
|
307
|
+
|
|
308
|
+
// THEN
|
|
309
|
+
expect(mockSignerSpy).toHaveBeenCalledWith(mockDerivationPath, {
|
|
310
|
+
transactions: [
|
|
311
|
+
"namespace-transaction-data",
|
|
312
|
+
"party-to-key-transaction-data",
|
|
313
|
+
"party-to-participant-transaction-data",
|
|
314
|
+
],
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { OnboardingPrepareResponse, PrepareTransferResponse } from "../../network/gateway";
|
|
2
|
+
import { PrepareTransactionResponse } from "../../types/onboard";
|
|
3
|
+
import { CantonSigner } from "../../types/signer";
|
|
4
|
+
import { splitTransaction } from "./split";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sign a Canton transaction - handles both prepared transactions and untyped versioned messages
|
|
8
|
+
*/
|
|
9
|
+
export async function signTransaction(
|
|
10
|
+
signer: CantonSigner,
|
|
11
|
+
derivationPath: string,
|
|
12
|
+
transactionData: PrepareTransferResponse | OnboardingPrepareResponse | PrepareTransactionResponse,
|
|
13
|
+
): Promise<string> {
|
|
14
|
+
let signature: string;
|
|
15
|
+
|
|
16
|
+
if ("json" in transactionData) {
|
|
17
|
+
const components = splitTransaction(transactionData.json);
|
|
18
|
+
signature = await signer.signTransaction(derivationPath, components);
|
|
19
|
+
} else {
|
|
20
|
+
const transactions = [
|
|
21
|
+
transactionData.transactions.namespace_transaction.serialized,
|
|
22
|
+
transactionData.transactions.party_to_key_transaction.serialized,
|
|
23
|
+
transactionData.transactions.party_to_participant_transaction.serialized,
|
|
24
|
+
];
|
|
25
|
+
signature = await signer.signTransaction(derivationPath, { transactions });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!signature || signature.length === 0) {
|
|
29
|
+
throw new Error("Device returned empty signature");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return signature;
|
|
33
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import prepareTransferMockSerialized from "../../test/prepare-transfer-serialized.json";
|
|
2
|
+
import prepareTransferMock from "../../test/prepare-transfer.json";
|
|
3
|
+
import { splitTransaction } from "./split";
|
|
4
|
+
|
|
5
|
+
function uint8ArrayToHex(bytes: Uint8Array): string {
|
|
6
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, "0")).join("");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("splitTransaction", () => {
|
|
10
|
+
it("should split transaction correctly", () => {
|
|
11
|
+
const transactionData = prepareTransferMock;
|
|
12
|
+
const result = splitTransaction(transactionData);
|
|
13
|
+
|
|
14
|
+
expect(result).toBeDefined();
|
|
15
|
+
expect(result.damlTransaction).toBeInstanceOf(Uint8Array);
|
|
16
|
+
expect(result.nodes).toBeInstanceOf(Array);
|
|
17
|
+
expect(result.metadata).toBeInstanceOf(Uint8Array);
|
|
18
|
+
expect(result.inputContracts).toBeInstanceOf(Array);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should properly serialize damlTransaction", () => {
|
|
22
|
+
const transactionData = prepareTransferMock;
|
|
23
|
+
const { damlTransaction } = splitTransaction(transactionData);
|
|
24
|
+
|
|
25
|
+
expect(uint8ArrayToHex(damlTransaction)).toEqual(prepareTransferMockSerialized.damlTransaction);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should properly serialize nodes", () => {
|
|
29
|
+
const transactionData = prepareTransferMock;
|
|
30
|
+
const { nodes } = splitTransaction(transactionData);
|
|
31
|
+
|
|
32
|
+
expect(nodes.map(uint8ArrayToHex)).toEqual(prepareTransferMockSerialized.nodes);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should properly serialize metadata", () => {
|
|
36
|
+
const transactionData = prepareTransferMock;
|
|
37
|
+
const { metadata } = splitTransaction(transactionData);
|
|
38
|
+
|
|
39
|
+
expect(uint8ArrayToHex(metadata)).toEqual(prepareTransferMockSerialized.metadata);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should properly serialize inputContracts", () => {
|
|
43
|
+
const transactionData = prepareTransferMock;
|
|
44
|
+
const { inputContracts } = splitTransaction(transactionData);
|
|
45
|
+
|
|
46
|
+
expect(inputContracts.map(uint8ArrayToHex)).toEqual(
|
|
47
|
+
prepareTransferMockSerialized.inputContracts,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import * as protobuf from "protobufjs";
|
|
2
|
+
import { PrepareTransferResponse } from "../../network/gateway";
|
|
3
|
+
import { CantonPreparedTransaction } from "../../types/signer";
|
|
4
|
+
import * as transactionProto from "../../types/transaction-proto.json";
|
|
5
|
+
|
|
6
|
+
const root: { [key: string]: any } = protobuf.Root.fromJSON(transactionProto) || {};
|
|
7
|
+
|
|
8
|
+
const RESERVED_WORDS = {
|
|
9
|
+
bool: "bool_",
|
|
10
|
+
enum: "enum_",
|
|
11
|
+
constructor: "constructor_",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const replaceReservedWords = (obj: any): any => {
|
|
15
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
16
|
+
if (Array.isArray(obj)) return obj.map(replaceReservedWords);
|
|
17
|
+
|
|
18
|
+
const transformed: any = {};
|
|
19
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
20
|
+
transformed[RESERVED_WORDS[key as keyof typeof RESERVED_WORDS] || key] =
|
|
21
|
+
replaceReservedWords(value);
|
|
22
|
+
}
|
|
23
|
+
return transformed;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Splits a Canton transaction into components for prepared transaction signing.
|
|
28
|
+
* Converts protobuf transaction data into structured components that can be
|
|
29
|
+
* sent to the Ledger device for signing.
|
|
30
|
+
*/
|
|
31
|
+
export function splitTransaction(
|
|
32
|
+
transaction: PrepareTransferResponse["json"],
|
|
33
|
+
): CantonPreparedTransaction {
|
|
34
|
+
const { transaction: transactionData, metadata } = transaction;
|
|
35
|
+
|
|
36
|
+
// Process DAML transaction
|
|
37
|
+
const DeviceDamlTransaction = root.lookupType(
|
|
38
|
+
"com.daml.ledger.api.v2.interactive.DeviceDamlTransaction",
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const damlTransactionBytes = DeviceDamlTransaction.encode({
|
|
42
|
+
version: transactionData.version,
|
|
43
|
+
roots: transactionData.roots,
|
|
44
|
+
nodesCount: transactionData.nodes?.length || 0,
|
|
45
|
+
nodeSeeds: (transactionData.nodeSeeds || []).map((seed: any) => ({
|
|
46
|
+
seed: Uint8Array.from(Buffer.from(seed.seed, "base64")),
|
|
47
|
+
...(seed.nodeId && seed.nodeId !== 0 && { nodeId: seed.nodeId }),
|
|
48
|
+
})),
|
|
49
|
+
}).finish();
|
|
50
|
+
|
|
51
|
+
// Process input contracts
|
|
52
|
+
const inputContracts = (metadata.inputContracts || []).map((contract: any) => {
|
|
53
|
+
const { eventBlob, ...contractWithoutBlob } = contract;
|
|
54
|
+
const InputContract = root.lookupType(
|
|
55
|
+
"com.daml.ledger.api.v2.interactive.DeviceMetadata.InputContract",
|
|
56
|
+
);
|
|
57
|
+
const contractPb = InputContract.fromObject(replaceReservedWords(contractWithoutBlob));
|
|
58
|
+
return InputContract.encode(contractPb).finish();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Process metadata
|
|
62
|
+
const metadataData = {
|
|
63
|
+
submitterInfo: {
|
|
64
|
+
actAs: metadata.submitterInfo.actAs,
|
|
65
|
+
commandId: metadata.submitterInfo.commandId,
|
|
66
|
+
},
|
|
67
|
+
synchronizerId: metadata.synchronizerId,
|
|
68
|
+
...(metadata.mediatorGroup !== undefined && { mediatorGroup: metadata.mediatorGroup }),
|
|
69
|
+
transactionUuid: metadata.transactionUuid,
|
|
70
|
+
submissionTime: Number.parseInt(metadata.preparationTime, 10),
|
|
71
|
+
inputContractsCount: metadata.inputContracts?.length || 0,
|
|
72
|
+
...(metadata.minLedgerEffectiveTime && {
|
|
73
|
+
minLedgerEffectiveTime: Number.parseInt(metadata.minLedgerEffectiveTime, 10),
|
|
74
|
+
}),
|
|
75
|
+
...(metadata.maxLedgerEffectiveTime && {
|
|
76
|
+
maxLedgerEffectiveTime: Number.parseInt(metadata.maxLedgerEffectiveTime, 10),
|
|
77
|
+
}),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const DeviceMetadata = root.lookupType("com.daml.ledger.api.v2.interactive.DeviceMetadata");
|
|
81
|
+
const metadataBytes = DeviceMetadata.encode(metadataData).finish();
|
|
82
|
+
|
|
83
|
+
// Process nodes
|
|
84
|
+
const nodesArray = transactionData.nodes || [];
|
|
85
|
+
const nodes = new Array(nodesArray.length);
|
|
86
|
+
|
|
87
|
+
for (const node of nodesArray) {
|
|
88
|
+
const nodeId = Number.parseInt(node.nodeId || "0", 10);
|
|
89
|
+
const Node = root.lookupType("com.daml.ledger.api.v2.interactive.DeviceDamlTransaction.Node");
|
|
90
|
+
const nodePb = Node.fromObject(replaceReservedWords(node));
|
|
91
|
+
const pos = nodesArray.length - 1 - nodeId;
|
|
92
|
+
nodes[pos] = Node.encode(nodePb).finish();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
damlTransaction: damlTransactionBytes,
|
|
97
|
+
nodes,
|
|
98
|
+
metadata: metadataBytes,
|
|
99
|
+
inputContracts,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -5,18 +5,9 @@ describe("utils", () => {
|
|
|
5
5
|
describe("isRecipientValid", () => {
|
|
6
6
|
it("should return true for valid Canton addresses", () => {
|
|
7
7
|
const validAddresses = [
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"a::0",
|
|
12
|
-
"party::999",
|
|
13
|
-
"user123::42",
|
|
14
|
-
"canton_1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z::123",
|
|
15
|
-
"test::123456789",
|
|
16
|
-
"abc::abc", // letters after ::
|
|
17
|
-
"test::ABC123", // mixed case letters and numbers
|
|
18
|
-
"user::a1b2c3", // alphanumeric after ::
|
|
19
|
-
"contract::XyZ789", // mixed case with numbers
|
|
8
|
+
"ldg::1220691e945dc1b210f3b6be9fbad73efaf642bfb96022552f66c9e2b83b00cb20e8",
|
|
9
|
+
"ldg-with-dash::1220691e945dc1b210f3b6be9fbad73efaf642bfb96022552f66c9e2b83b00cb20e8",
|
|
10
|
+
"ldg-with-number-1::1220691e945dc1b210f3b6be9fbad73efaf642bfb96022552f66c9e2b83b00cb20e8",
|
|
20
11
|
];
|
|
21
12
|
|
|
22
13
|
validAddresses.forEach(address => {
|
|
@@ -26,6 +17,8 @@ describe("utils", () => {
|
|
|
26
17
|
|
|
27
18
|
it("should return false for invalid Canton addresses", () => {
|
|
28
19
|
const invalidAddresses = [
|
|
20
|
+
"ldg::1220691e945dc1b210f3b6be9fbad73efaf642bfb96022552f66c9e2b83b00cb20e", // too short fingerprint
|
|
21
|
+
"ldg::1220691e945dc1b210f3b6be9fbad73efaf642bfb96022552f66c9e2b83b00cb20e80", // too long fingerprint
|
|
29
22
|
"", // empty string
|
|
30
23
|
"::123", // no characters before ::
|
|
31
24
|
"abc::", // no characters after ::
|
|
@@ -53,26 +46,25 @@ describe("utils", () => {
|
|
|
53
46
|
});
|
|
54
47
|
|
|
55
48
|
it("should handle edge cases", () => {
|
|
56
|
-
|
|
57
|
-
expect(isRecipientValid(
|
|
58
|
-
expect(isRecipientValid(
|
|
59
|
-
expect(isRecipientValid(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
expect(isRecipientValid("
|
|
63
|
-
expect(isRecipientValid("
|
|
64
|
-
expect(isRecipientValid("
|
|
65
|
-
expect(isRecipientValid(".::
|
|
49
|
+
const validHex68 = "1220691e945dc1b210f3b6be9fbad73efaf642bfb96022552f66c9e2b83b00cb20e8";
|
|
50
|
+
expect(isRecipientValid(`a::${validHex68}`)).toBe(true); // single letter prefix
|
|
51
|
+
expect(isRecipientValid(`1::${validHex68}`)).toBe(true); // single number prefix
|
|
52
|
+
expect(isRecipientValid(`a-1::${validHex68}`)).toBe(true); // prefix with dash
|
|
53
|
+
|
|
54
|
+
// Invalid edge cases (should be false)
|
|
55
|
+
expect(isRecipientValid("a::1")).toBe(false); // too short hex
|
|
56
|
+
expect(isRecipientValid("a::a")).toBe(false); // too short hex
|
|
57
|
+
expect(isRecipientValid("_::1")).toBe(false); // invalid prefix character
|
|
58
|
+
expect(isRecipientValid(".::1")).toBe(false); // invalid prefix character
|
|
66
59
|
});
|
|
67
60
|
|
|
68
|
-
it("should
|
|
69
|
-
|
|
70
|
-
expect(isRecipientValid(" abc::
|
|
71
|
-
expect(isRecipientValid("
|
|
72
|
-
expect(isRecipientValid("abc ::
|
|
73
|
-
expect(isRecipientValid("abc
|
|
74
|
-
expect(isRecipientValid("abc::
|
|
75
|
-
expect(isRecipientValid("abc::abc::def")).toBe(true); // multiple :: with letters
|
|
61
|
+
it("should reject addresses with spaces and multiple colons", () => {
|
|
62
|
+
expect(isRecipientValid(" abc::123")).toBe(false); // space before address
|
|
63
|
+
expect(isRecipientValid(" abc::abc")).toBe(false); // space before address with letters
|
|
64
|
+
expect(isRecipientValid("abc ::123")).toBe(false); // space before ::
|
|
65
|
+
expect(isRecipientValid("abc ::abc")).toBe(false); // space before :: with letters
|
|
66
|
+
expect(isRecipientValid("abc::123::456")).toBe(false); // multiple ::
|
|
67
|
+
expect(isRecipientValid("abc::abc::def")).toBe(false); // multiple :: with letters
|
|
76
68
|
});
|
|
77
69
|
});
|
|
78
70
|
|
|
@@ -8,9 +8,9 @@ export const validateTag = (tag: BigNumber) => {
|
|
|
8
8
|
);
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
const CANTON_ADDRESS_REGEX =
|
|
11
|
+
const CANTON_ADDRESS_REGEX = /^[a-zA-Z0-9-]+::[a-fA-F0-9]{68}$/;
|
|
12
12
|
|
|
13
13
|
export function isRecipientValid(recipient: string): boolean {
|
|
14
|
-
// Canton address format:
|
|
14
|
+
// Canton address format: alphanumeric with dashes :: 68 hexadecimal characters
|
|
15
15
|
return CANTON_ADDRESS_REGEX.test(recipient);
|
|
16
16
|
}
|
|
@@ -107,6 +107,8 @@ describe("gateway (devnet)", () => {
|
|
|
107
107
|
testIfPrepared(
|
|
108
108
|
"should not throw when already onboarded",
|
|
109
109
|
async () => {
|
|
110
|
+
// Add delay to ensure previous operations are complete
|
|
111
|
+
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
110
112
|
// GIVEN
|
|
111
113
|
const { keyPair } = getOnboardedAccount();
|
|
112
114
|
const signature = keyPair.sign(prepareResponse!.transactions.combined_hash);
|
package/src/network/gateway.ts
CHANGED
|
@@ -283,7 +283,8 @@ export type OperationInfo =
|
|
|
283
283
|
const getGatewayUrl = (currency: CryptoCurrency) => coinConfig.getCoinConfig(currency).gatewayUrl;
|
|
284
284
|
const getNodeId = (currency: CryptoCurrency) =>
|
|
285
285
|
coinConfig.getCoinConfig(currency).nodeId || "ledger-live-devnet";
|
|
286
|
-
const getNetworkType = (currency: CryptoCurrency) =>
|
|
286
|
+
export const getNetworkType = (currency: CryptoCurrency) =>
|
|
287
|
+
coinConfig.getCoinConfig(currency).networkType;
|
|
287
288
|
|
|
288
289
|
const gatewayNetwork = <T, U = unknown>(req: LiveNetworkRequest<U>) => {
|
|
289
290
|
const API_KEY = getEnv("CANTON_API_KEY");
|
|
@@ -448,13 +449,6 @@ export async function prepareTapRequest(
|
|
|
448
449
|
currency: CryptoCurrency,
|
|
449
450
|
{ partyId, amount = 1000000 }: PrepareTapRequest,
|
|
450
451
|
) {
|
|
451
|
-
if (getNetworkType(currency) === "mainnet") {
|
|
452
|
-
return {
|
|
453
|
-
serialized: "",
|
|
454
|
-
json: null,
|
|
455
|
-
hash: "",
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
452
|
const { data } = await gatewayNetwork<PrepareTapResponse, { amount: string; type: string }>({
|
|
459
453
|
method: "POST",
|
|
460
454
|
url: `${getGatewayUrl(currency)}/v1/node/${getNodeId(currency)}/party/${partyId}/transaction/prepare`,
|
|
@@ -505,6 +499,7 @@ export async function prepareTransferRequest(
|
|
|
505
499
|
url: `${getGatewayUrl(currency)}/v1/node/${getNodeId(currency)}/party/${partyId}/transaction/prepare`,
|
|
506
500
|
data: params,
|
|
507
501
|
});
|
|
502
|
+
|
|
508
503
|
return data;
|
|
509
504
|
}
|
|
510
505
|
|