@chorus-one/signer-fireblocks
Version:
Fireblocks signer for Chorus One SDK
192 lines (191 loc) • 9.14 kB
JavaScript
;
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;