UNPKG

@chorus-one/signer-fireblocks

Version:

Fireblocks signer for Chorus One SDK

192 lines (191 loc) 9.14 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.newFireblocksSignerBackend = exports.FireblocksSigner = void 0; const fireblocks_sdk_1 = require("fireblocks-sdk"); const fireblocks_sdk_2 = require("fireblocks-sdk"); const utils_1 = require("@chorus-one/utils"); const authProvider_1 = require("./authProvider"); /** * The FireblocksSigner in the Chorus One SDK is a specialized implementation of the Signer interface that integrates * with the Fireblocks platform. * * Fireblocks is known for its advanced security features, including multi-party computation (MPC) and secure wallet * infrastructure, making it an ideal choice for enterprises requiring robust security and compliance. */ class FireblocksSigner { config; fireblocksClient; vault; addressDerivationFn; accounts; logger; /** * Constructs a new FireblocksSigner. * * @param params - The parameters required to initialize the FireblocksSigner * @param params.apiSecretKey - Fireblocks API Secret key * @param params.apiKey - Fireblocks API Key * @param params.vaultName - The name of the Fireblocks vault where the assets are stored * @param params.assetId - The identifier for the asset you intend to manage * @param params.addressDerivationFn - A function that derives the address from the public key * @param params.timeout - (Optional) The maximum time (in ms) to wait for the Fireblocks API sign request to complete * @param params.pollInterval - (Optional) The interval (in ms) at which the signer polls the Fireblocks API to check if the sign request has completed * @param params.apiUrl - (Optional) The URL of the Fireblocks API, defaults to `https://api.fireblocks.io` * @param params.logger - (Optional) A logger to use for logging messages, i.e `console` * * @returns A new instance of FireblocksSigner. */ constructor(params) { const { addressDerivationFn, ...config } = params; this.config = { ...config, apiUrl: params.apiUrl ?? 'https://api.fireblocks.io' }; this.logger = params.logger ?? utils_1.nopLogger; this.addressDerivationFn = addressDerivationFn; this.accounts = new Map(); } /** * Initializes the signer, performing any necessary setup or configuration. * @returns A promise that resolves once the initialization is complete. */ async init() { const backend = await newFireblocksSignerBackend(this.config); const vaults = await backend .getVaultAccountsWithPageInfo({ namePrefix: this.config.vaultName }) .then((res) => { return res.accounts.filter((account) => account.name === this.config.vaultName); }); if (vaults.length !== 1) { throw new Error('fireblocks vault name not found, expecte exactly 1 result, got: ' + vaults.length); } this.fireblocksClient = backend; this.vault = vaults[0]; // BIP44 format: m / purpose' / coin_type' / account' / change / address_index const response = await this.fireblocksClient.getMaxBip44IndexUsed(this.vault.id, this.config.assetId); const maxChangeAddress = response.maxBip44ChangeAddressIndexUsed ?? 0; const maxAddressIndex = response.maxBip44AddressIndexUsed ?? 0; const promises = []; for (let change = 0; change <= maxChangeAddress; change++) { for (let addressIndex = 0; addressIndex <= maxAddressIndex; addressIndex++) { promises.push(this.getPublicKeyInfoForVaultAccount(change, addressIndex)); } } const publicKeysResponse = await Promise.all(promises); publicKeysResponse.forEach(async (response) => { const pk = Uint8Array.from(Buffer.from(response.publicKey, 'hex')); const pth = response.derivationPath; // fireblocks hardens the first 3 indexes const derivationPath = `m/${pth[0]}'/${pth[1]}'/${pth[2]}'/${pth[3]}/${pth[4]}`; const derivedAddresses = await this.addressDerivationFn(pk, derivationPath); derivedAddresses.forEach((address) => { this.accounts.set(address.toLowerCase(), { hdPath: derivationPath, publicKey: pk }); }); }); } /** * Signs the provided data using the private key associated with the signer's address. * * @param signerAddress - The address of the signer * @param signerData - The data to be signed, which can be a raw message or custom data * @param options - Additional options * @param options.note - An optional note to include with the transaction * * @returns A promise that resolves to an object containing the signature and public key. */ async sign(signerAddress, signerData, options) { if (!this.vault) { throw new Error('FireblocksSigner instance is not initialized'); } const args = { assetId: this.config.assetId, source: { type: fireblocks_sdk_1.PeerType.VAULT_ACCOUNT, id: this.vault.id, address: signerAddress }, note: options?.note ?? '', operation: fireblocks_sdk_1.TransactionOperation.RAW, extraParameters: { rawMessageData: { messages: [ { content: signerData.message } ] } } }; // https://developers.fireblocks.com/docs/raw-message-signing this.logger.info('wait for the TX signature from the remote signer'); if (!this.fireblocksClient) { throw new Error('FireblocksSigner instance is not initialized'); } const { id } = await this.fireblocksClient.createTransaction(args); let txInfo = await this.fireblocksClient.getTransactionById(id); let status = txInfo.status; const states = [fireblocks_sdk_1.TransactionStatus.COMPLETED, fireblocks_sdk_1.TransactionStatus.FAILED, fireblocks_sdk_1.TransactionStatus.BLOCKED]; const pollInterval = this.config.pollInterval ?? 1000; const startTime = Date.now(); while (!states.some((x) => x === status)) { try { this.logger.info(`* signer request ID: ${id} with status: ${status}`); txInfo = await this.fireblocksClient.getTransactionById(id); status = txInfo.status; } catch (err) { this.logger.error('probing remote signer failed', err); } // trigger timeout if the signer takes too long if (this.config.timeout !== undefined && Date.now() - startTime > this.config.timeout) { throw new Error('timeout waiting for the signer to complete'); } await new Promise((resolve, _) => setTimeout(resolve, pollInterval)); } const details = txInfo.subStatus === '' ? 'none' : txInfo.subStatus; this.logger.info(`* signer request ID finished with status ${status}; details: ${details}`); if (txInfo.signedMessages === undefined || txInfo.signedMessages.length !== 1) { throw new Error(('expected exactly 1 signed message, got: ' + (txInfo.signedMessages === undefined ? 0 : txInfo.signedMessages.length))); } return { sig: txInfo.signedMessages[0].signature, pk: Uint8Array.from(Buffer.from(txInfo.signedMessages[0].publicKey, 'hex')) }; } /** * Retrieves the public key associated with the signer's address. * * @param address - The address of the signer * * @returns A promise that resolves to a Uint8Array representing the public key. */ async getPublicKey(address) { const account = this.accounts.get(address.toLowerCase()); if (account === undefined) { throw new Error(`no public key found for address: ${address}`); } return account.publicKey; } async getPublicKeyInfoForVaultAccount(change, addressIndex) { if (!this.fireblocksClient || !this.vault) { throw new Error('FireblocksSigner instance is not initialized'); } const pubKeyArgs = { assetId: this.config.assetId, vaultAccountId: Number.parseInt(this.vault.id), change, addressIndex }; return await this.fireblocksClient.getPublicKeyInfoForVaultAccount(pubKeyArgs); } } exports.FireblocksSigner = FireblocksSigner; async function newFireblocksSignerBackend(config) { const { apiSecretKey, apiKey, apiUrl } = config; const authProvider = (0, authProvider_1.getAuthProvider)(apiSecretKey, apiKey); return new fireblocks_sdk_2.FireblocksSDK(apiSecretKey.trim(), apiKey.trim(), apiUrl, authProvider); } exports.newFireblocksSignerBackend = newFireblocksSignerBackend;