@canton-network/core-signing-fireblocks 0.8.1 → 0.10.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/dist/index.js CHANGED
@@ -1,164 +1,452 @@
1
- // Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2
- // SPDX-License-Identifier: Apache-2.0
3
- // Disabled unused vars rule to allow for future implementations
4
- /* eslint-disable @typescript-eslint/no-unused-vars */
5
- import { buildController, PartyMode, SigningProvider, } from '@canton-network/core-signing-lib';
6
- import { FireblocksHandler } from './fireblocks.js';
7
- import _ from 'lodash';
1
+ import { PartyMode, SigningProvider, CC_COIN_TYPE, buildController } from '@canton-network/core-signing-lib';
2
+ import { Fireblocks, PublicKeyInformationAlgorithmEnum } from '@fireblocks/ts-sdk';
3
+ import { pino } from 'pino';
8
4
  import { z } from 'zod';
9
- const FireblocksApiKeyInfoSchema = z.object({
10
- apiKey: z.string(),
11
- apiSecret: z.string(),
5
+ import _ from 'lodash';
6
+
7
+ var __defProp = Object.defineProperty;
8
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
9
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
10
+ var RawMessageSchema = z.object({
11
+ content: z.string(),
12
+ derivationPath: z.array(z.number())
13
+ });
14
+ var RawMessageDataSchema = z.object({
15
+ messages: z.array(RawMessageSchema),
16
+ algorithm: z.string()
12
17
  });
13
- const FireblocksConfigSchema = z.object({
14
- defaultApiKey: FireblocksApiKeyInfoSchema.optional(),
15
- userApiKeys: z.map(z.string(), FireblocksApiKeyInfoSchema),
16
- apiPath: z.string().optional(),
18
+ var RawMessageExtraParametersSchema = z.object({
19
+ rawMessageData: RawMessageDataSchema
17
20
  });
18
- const createFireblocksHandler = (config) => {
19
- return new FireblocksHandler(config.defaultKeyInfo
20
- ? {
21
- apiKey: config.defaultKeyInfo.apiKey,
22
- apiSecret: config.defaultKeyInfo.apiSecret,
21
+ var logger = pino({ name: "main", level: "debug" });
22
+ var FireblocksHandler = class {
23
+ constructor(defaultKey, userKeys, apiPath = "https://api.fireblocks.io/v1") {
24
+ __publicField(this, "defaultClient");
25
+ __publicField(this, "clients", /* @__PURE__ */ new Map());
26
+ __publicField(this, "keyInfoByPublicKey", /* @__PURE__ */ new Map());
27
+ __publicField(this, "publicKeyByDerivationPath", /* @__PURE__ */ new Map());
28
+ __publicField(this, "getClient", (userId) => {
29
+ if (userId !== void 0 && this.clients.has(userId)) {
30
+ return this.clients.get(userId);
31
+ } else if (this.defaultClient) {
32
+ return this.defaultClient;
33
+ } else {
34
+ throw new Error("No Fireblocks client available for this user.");
35
+ }
36
+ });
37
+ if (defaultKey) {
38
+ this.defaultClient = new Fireblocks({
39
+ apiKey: defaultKey.apiKey,
40
+ basePath: apiPath,
41
+ secretKey: defaultKey.apiSecret
42
+ });
43
+ }
44
+ userKeys.forEach((keyInfo, userId) => {
45
+ const client = new Fireblocks({
46
+ apiKey: keyInfo.apiKey,
47
+ basePath: apiPath,
48
+ secretKey: keyInfo.apiSecret
49
+ });
50
+ this.clients.set(userId, client);
51
+ });
52
+ }
53
+ /**
54
+ * Get all public keys which correspond to Fireblocks vault accounts. This will
55
+ * also refresh the key cache.
56
+ * @returns List of Fireblocks public key information
57
+ */
58
+ async getPublicKeys(userId) {
59
+ const keys = [];
60
+ try {
61
+ const client = this.getClient(userId);
62
+ const vaultAccounts = [];
63
+ let after = void 0;
64
+ do {
65
+ const resp = await client.vaults.getPagedVaultAccounts(
66
+ after ? { after } : {}
67
+ );
68
+ after = resp.data.paging?.after;
69
+ vaultAccounts.push(...resp.data.accounts || []);
70
+ } while (after !== void 0);
71
+ for (const vault of vaultAccounts) {
72
+ if (vault.id) {
73
+ const derivationPath = [
74
+ 44,
75
+ CC_COIN_TYPE,
76
+ Number(vault.id) || 0,
77
+ 0,
78
+ 0
79
+ ];
80
+ const publicKey = await this.lookupPublicKey(
81
+ userId,
82
+ derivationPath
83
+ );
84
+ const storedKey = {
85
+ derivationPath,
86
+ publicKey,
87
+ name: vault.name || vault.id,
88
+ algorithm: PublicKeyInformationAlgorithmEnum.EddsaEd25519
89
+ };
90
+ keys.push(storedKey);
91
+ this.keyInfoByPublicKey.set(storedKey.publicKey, storedKey);
23
92
  }
24
- : undefined, config.userApiKeys, config.apiPath || 'https://api.fireblocks.io/v1');
25
- };
26
- export default class FireblocksSigningDriver {
27
- fireblocks;
28
- config;
29
- constructor(config) {
30
- this.config = config;
31
- this.fireblocks = createFireblocksHandler(config);
93
+ }
94
+ } catch (error) {
95
+ logger.error(error, "Error fetching vault accounts:");
96
+ throw error;
32
97
  }
33
- partyMode = PartyMode.EXTERNAL;
34
- signingProvider = SigningProvider.FIREBLOCKS;
35
- controller = (userId) => buildController({
36
- signTransaction: async (params) => {
37
- // TODO: validate transaction here
38
- try {
39
- const tx = await this.fireblocks.signTransaction(userId, params.txHash, params.publicKey, params.internalTxId);
40
- return {
41
- txId: tx.txId,
42
- status: tx.status,
43
- signature: tx.signature,
44
- publicKey: tx.publicKey,
45
- };
46
- }
47
- catch (error) {
48
- return {
49
- error: 'signing_error',
50
- error_description: error.message,
51
- };
52
- }
53
- },
54
- getTransaction: async (params) => {
55
- const tx = await this.fireblocks.getTransaction(userId, params.txId);
56
- if (tx) {
57
- return {
58
- txId: tx.txId,
59
- status: tx.status,
60
- signature: tx.signature,
61
- publicKey: tx.publicKey,
62
- };
63
- }
64
- else {
65
- return {
66
- error: 'transaction_not_found',
67
- error_description: 'The requested transaction does not exist.',
68
- };
69
- }
70
- },
71
- getTransactions: async (params) => {
72
- const transactions = [];
73
- if (params.publicKeys || params.txIds) {
74
- const txIds = new Set(params.txIds);
75
- const publicKeys = new Set(params.publicKeys);
76
- for await (const tx of this.fireblocks.getTransactions(userId)) {
77
- if (txIds.has(tx.txId) ||
78
- publicKeys.has(tx.publicKey || '')) {
79
- transactions.push({
80
- txId: tx.txId,
81
- status: tx.status,
82
- signature: tx.signature,
83
- publicKey: tx.publicKey,
84
- });
85
- }
86
- if (params.txIds &&
87
- !params.publicKeys &&
88
- transactions.length == txIds.size) {
89
- // stop if we are filtering by only txIds and have found all requested transactions
90
- break;
91
- }
98
+ return keys;
99
+ }
100
+ /**
101
+ * Takes a Fireblocks response from a transactions call and extracts the transaction information
102
+ * relevant to the Wallet Gateway. This will potentially fetch the public key since unsigned transactions
103
+ * do not include it
104
+ * @returns FireblocksTransaction
105
+ */
106
+ async formatTransaction(userId, tx) {
107
+ if (tx.signedMessages && tx.signedMessages.length > 0) {
108
+ const signedMessage = tx.signedMessages[0];
109
+ if (!signedMessage.publicKey || !signedMessage.content || !signedMessage.signature) {
110
+ return void 0;
111
+ }
112
+ return {
113
+ txId: tx.id,
114
+ status: "signed",
115
+ createdAt: tx.createdAt,
116
+ publicKey: signedMessage.publicKey,
117
+ signature: signedMessage.signature.fullSig,
118
+ derivationPath: signedMessage.derivationPath
119
+ };
120
+ } else {
121
+ const rawMessageData = RawMessageExtraParametersSchema.safeParse(
122
+ tx.extraParameters
123
+ );
124
+ if (!rawMessageData.success) {
125
+ return void 0;
126
+ }
127
+ const message = rawMessageData.data.rawMessageData.messages[0];
128
+ const publicKey = await this.lookupPublicKey(
129
+ userId,
130
+ message.derivationPath
131
+ );
132
+ const status = tx.status === "REJECTED" || tx.status === "BLOCKED" ? "rejected" : tx.status === "FAILED" ? "failed" : "pending";
133
+ return {
134
+ txId: tx.id,
135
+ status,
136
+ createdAt: tx.createdAt,
137
+ publicKey,
138
+ derivationPath: message.derivationPath
139
+ };
140
+ }
141
+ }
142
+ /**
143
+ * Looks up or fetches the public key (only) for a given derivation path
144
+ * @returns The public key as a string
145
+ */
146
+ async lookupPublicKey(userId, derivationPath) {
147
+ const derivationPathString = JSON.stringify(derivationPath);
148
+ if (this.publicKeyByDerivationPath.has(derivationPathString)) {
149
+ return this.publicKeyByDerivationPath.get(derivationPathString);
150
+ } else {
151
+ try {
152
+ const client = this.getClient(userId);
153
+ const key = await client.vaults.getPublicKeyInfo({
154
+ algorithm: PublicKeyInformationAlgorithmEnum.EddsaEd25519,
155
+ derivationPath: derivationPathString
156
+ });
157
+ if (key.data.publicKey) {
158
+ this.publicKeyByDerivationPath.set(
159
+ derivationPathString,
160
+ key.data.publicKey
161
+ );
162
+ return key.data.publicKey;
163
+ } else {
164
+ throw new Error(
165
+ "Malformed public key response from Fireblocks"
166
+ );
167
+ }
168
+ } catch (error) {
169
+ throw new Error(`Error looking up public key: ${error}`);
170
+ }
171
+ }
172
+ }
173
+ /**
174
+ * Fetch a single RAW transaction from Fireblocks by its transaction ID
175
+ * @returns FireblocksTransaction or undefined if not found
176
+ */
177
+ async getTransaction(userId, txId) {
178
+ try {
179
+ const client = this.getClient(userId);
180
+ const transaction = await client.transactions.getTransaction({
181
+ txId
182
+ });
183
+ return await this.formatTransaction(userId, transaction.data);
184
+ } catch {
185
+ return void 0;
186
+ }
187
+ }
188
+ /**
189
+ * Get all RAW transactions from Fireblocks. Returns an async generator as
190
+ * this may return a large number of transactions and will occasionally need to
191
+ * refresh the key cache.
192
+ * @returns AsyncGenerator of FireblocksTransactions
193
+ */
194
+ async *getTransactions(userId, {
195
+ limit = 200,
196
+ before
197
+ } = {}) {
198
+ let fetchedLength = 0;
199
+ let beforeQuery = before;
200
+ try {
201
+ const client = this.getClient(userId);
202
+ do {
203
+ const transactions = await client.transactions.getTransactions({
204
+ sourceType: "VAULT_ACCOUNT",
205
+ limit,
206
+ ...beforeQuery ? { before: beforeQuery.toString() } : {}
207
+ });
208
+ fetchedLength = transactions.data.length;
209
+ for (const tx of transactions.data) {
210
+ beforeQuery = tx.createdAt - 1;
211
+ const formatTransaction = await this.formatTransaction(
212
+ userId,
213
+ tx
214
+ );
215
+ if (formatTransaction) {
216
+ yield formatTransaction;
217
+ } else {
218
+ continue;
219
+ }
220
+ }
221
+ } while (fetchedLength > 0);
222
+ } catch (error) {
223
+ logger.error(error, "Error fetching signatures");
224
+ throw error;
225
+ }
226
+ }
227
+ /**
228
+ * Sign a transaction using a public key
229
+ * @param tx - The transaction to sign, as a string
230
+ * @param publicKey - The public key to use for signing
231
+ * @param externalTxId - The transaction ID assigned by the Wallet Gateway
232
+ * @return The transaction object from Fireblocks
233
+ */
234
+ async signTransaction(userId, tx, publicKey, externalTxId) {
235
+ try {
236
+ const client = this.getClient(userId);
237
+ if (!this.keyInfoByPublicKey.has(publicKey)) {
238
+ await this.getPublicKeys(userId);
239
+ }
240
+ const key = this.keyInfoByPublicKey.get(publicKey);
241
+ if (!key) {
242
+ throw new Error(`Public key ${publicKey} not found in vaults`);
243
+ }
244
+ const transaction = await client.transactions.createTransaction({
245
+ transactionRequest: {
246
+ operation: "RAW",
247
+ note: `Signing transaction with public key ${publicKey}`,
248
+ externalTxId,
249
+ extraParameters: {
250
+ rawMessageData: {
251
+ messages: [
252
+ {
253
+ content: tx,
254
+ derivationPath: key.derivationPath
92
255
  }
93
- return {
94
- transactions: transactions,
95
- };
256
+ ],
257
+ algorithm: key.algorithm
96
258
  }
97
- else {
98
- return {
99
- error: 'bad_arguments',
100
- error_description: 'either public key or txIds must be supplied',
101
- };
102
- }
103
- },
104
- getKeys: async () => {
105
- try {
106
- const keys = await this.fireblocks.getPublicKeys(userId);
107
- return {
108
- keys: keys.map((k) => ({
109
- id: k.derivationPath.join('-'),
110
- name: k.name,
111
- publicKey: k.publicKey,
112
- })),
113
- };
114
- }
115
- catch (error) {
116
- return {
117
- error: 'fetch_error',
118
- error_description: error.message,
119
- };
120
- }
121
- },
122
- createKey: async (_params) => {
123
- return {
124
- error: 'not_allowed',
125
- error_description: 'Creating a Fireblocks key through the Wallet Gateway is not allowed, please create new keys directly in Fireblocks.',
126
- };
127
- },
128
- getConfiguration: async () => {
129
- const hideFireblocksKeySecret = (keyInfo) => {
130
- return keyInfo
131
- ? {
132
- apiKey: keyInfo.apiKey,
133
- apiSecret: '***HIDDEN***',
134
- }
135
- : undefined;
136
- };
137
- return {
138
- ...this.config,
139
- defaultKeyInfo: hideFireblocksKeySecret(this.config.defaultKeyInfo),
140
- userApiKeys: new Map([...this.config.userApiKeys].map(([k, v]) => [
141
- k,
142
- hideFireblocksKeySecret(v),
143
- ])),
144
- };
145
- },
146
- setConfiguration: async (params) => {
147
- const validated = FireblocksConfigSchema.safeParse(params);
148
- if (!validated.success) {
149
- return {
150
- error: 'bad_arguments',
151
- error_description: validated.error.message,
152
- };
259
+ }
260
+ }
261
+ });
262
+ let status = "pending";
263
+ switch (transaction.data.status) {
264
+ case "REJECTED":
265
+ status = "rejected";
266
+ break;
267
+ case "COMPLETED":
268
+ status = "signed";
269
+ break;
270
+ case "CANCELLED":
271
+ case "FAILED":
272
+ case "BLOCKED":
273
+ status = "failed";
274
+ break;
275
+ }
276
+ return {
277
+ txId: transaction.data.id,
278
+ status,
279
+ publicKey: key.publicKey,
280
+ derivationPath: key.derivationPath
281
+ };
282
+ } catch (error) {
283
+ logger.error(error, "Error signing transaction:");
284
+ throw error;
285
+ }
286
+ }
287
+ };
288
+ var FireblocksApiKeyInfoSchema = z.object({
289
+ apiKey: z.string(),
290
+ apiSecret: z.string()
291
+ });
292
+ var FireblocksConfigSchema = z.object({
293
+ defaultApiKey: FireblocksApiKeyInfoSchema.optional(),
294
+ userApiKeys: z.map(z.string(), FireblocksApiKeyInfoSchema),
295
+ apiPath: z.string().optional()
296
+ });
297
+ var createFireblocksHandler = (config) => {
298
+ return new FireblocksHandler(
299
+ config.defaultKeyInfo ? {
300
+ apiKey: config.defaultKeyInfo.apiKey,
301
+ apiSecret: config.defaultKeyInfo.apiSecret
302
+ } : void 0,
303
+ config.userApiKeys,
304
+ config.apiPath || "https://api.fireblocks.io/v1"
305
+ );
306
+ };
307
+ var FireblocksSigningDriver = class {
308
+ constructor(config) {
309
+ __publicField(this, "fireblocks");
310
+ __publicField(this, "config");
311
+ __publicField(this, "partyMode", PartyMode.EXTERNAL);
312
+ __publicField(this, "signingProvider", SigningProvider.FIREBLOCKS);
313
+ __publicField(this, "controller", (userId) => buildController({
314
+ signTransaction: async (params) => {
315
+ try {
316
+ const tx = await this.fireblocks.signTransaction(
317
+ userId,
318
+ params.txHash,
319
+ params.publicKey,
320
+ params.internalTxId
321
+ );
322
+ return {
323
+ txId: tx.txId,
324
+ status: tx.status,
325
+ signature: tx.signature,
326
+ publicKey: tx.publicKey
327
+ };
328
+ } catch (error) {
329
+ return {
330
+ error: "signing_error",
331
+ error_description: error.message
332
+ };
333
+ }
334
+ },
335
+ getTransaction: async (params) => {
336
+ const tx = await this.fireblocks.getTransaction(
337
+ userId,
338
+ params.txId
339
+ );
340
+ if (tx) {
341
+ return {
342
+ txId: tx.txId,
343
+ status: tx.status,
344
+ signature: tx.signature,
345
+ publicKey: tx.publicKey
346
+ };
347
+ } else {
348
+ return {
349
+ error: "transaction_not_found",
350
+ error_description: "The requested transaction does not exist."
351
+ };
352
+ }
353
+ },
354
+ getTransactions: async (params) => {
355
+ const transactions = [];
356
+ if (params.publicKeys || params.txIds) {
357
+ const txIds = new Set(params.txIds);
358
+ const publicKeys = new Set(params.publicKeys);
359
+ for await (const tx of this.fireblocks.getTransactions(
360
+ userId
361
+ )) {
362
+ if (txIds.has(tx.txId) || publicKeys.has(tx.publicKey || "")) {
363
+ transactions.push({
364
+ txId: tx.txId,
365
+ status: tx.status,
366
+ signature: tx.signature,
367
+ publicKey: tx.publicKey
368
+ });
153
369
  }
154
- if (!_.isEqual(validated.data, this.config)) {
155
- this.config = validated.data;
156
- this.fireblocks = createFireblocksHandler(this.config);
370
+ if (params.txIds && !params.publicKeys && transactions.length == txIds.size) {
371
+ break;
157
372
  }
158
- return params;
159
- },
160
- // TODO: implement subscribeTransactions - we will need to figure out how to handle subscriptions
161
- // when the controller is not running in a server context
162
- subscribeTransactions: async (params) => Promise.resolve({}),
163
- });
164
- }
373
+ }
374
+ return {
375
+ transactions
376
+ };
377
+ } else {
378
+ return {
379
+ error: "bad_arguments",
380
+ error_description: "either public key or txIds must be supplied"
381
+ };
382
+ }
383
+ },
384
+ getKeys: async () => {
385
+ try {
386
+ const keys = await this.fireblocks.getPublicKeys(userId);
387
+ return {
388
+ keys: keys.map((k) => ({
389
+ id: k.derivationPath.join("-"),
390
+ name: k.name,
391
+ publicKey: k.publicKey
392
+ }))
393
+ };
394
+ } catch (error) {
395
+ return {
396
+ error: "fetch_error",
397
+ error_description: error.message
398
+ };
399
+ }
400
+ },
401
+ createKey: async (_params) => {
402
+ return {
403
+ error: "not_allowed",
404
+ error_description: "Creating a Fireblocks key through the Wallet Gateway is not allowed, please create new keys directly in Fireblocks."
405
+ };
406
+ },
407
+ getConfiguration: async () => {
408
+ const hideFireblocksKeySecret = (keyInfo) => {
409
+ return keyInfo ? {
410
+ apiKey: keyInfo.apiKey,
411
+ apiSecret: "***HIDDEN***"
412
+ } : void 0;
413
+ };
414
+ return {
415
+ ...this.config,
416
+ defaultKeyInfo: hideFireblocksKeySecret(
417
+ this.config.defaultKeyInfo
418
+ ),
419
+ userApiKeys: new Map(
420
+ [...this.config.userApiKeys].map(([k, v]) => [
421
+ k,
422
+ hideFireblocksKeySecret(v)
423
+ ])
424
+ )
425
+ };
426
+ },
427
+ setConfiguration: async (params) => {
428
+ const validated = FireblocksConfigSchema.safeParse(params);
429
+ if (!validated.success) {
430
+ return {
431
+ error: "bad_arguments",
432
+ error_description: validated.error.message
433
+ };
434
+ }
435
+ if (!_.isEqual(validated.data, this.config)) {
436
+ this.config = validated.data;
437
+ this.fireblocks = createFireblocksHandler(this.config);
438
+ }
439
+ return params;
440
+ },
441
+ // TODO: implement subscribeTransactions - we will need to figure out how to handle subscriptions
442
+ // when the controller is not running in a server context
443
+ subscribeTransactions: async (params) => Promise.resolve({})
444
+ }));
445
+ this.config = config;
446
+ this.fireblocks = createFireblocksHandler(config);
447
+ }
448
+ };
449
+
450
+ export { FireblocksSigningDriver as default };
451
+ //# sourceMappingURL=index.js.map
452
+ //# sourceMappingURL=index.js.map