Raw Message Signing Example

Structuring The Raw Signing request


  1. Add extraParameters
  2. Within this object, add rawMessageData
  3. Within that object, add a messages array.

{
  assetId: "assetId",
  operation: TransactionOperation.RAW,
  source: {
    type: PeerType.VAULT_ACCOUNT,
    id: "vaultAccountId",
  },
  note: ``,
  extraParameters: {
    rawMessageData: {
      algorithm: "MPC_ECDSA_SECP256K1"/"MPC_ECDSA_SECP256R1"/"MPC_EDDSA_ED25519", // Optional
      messages: [
       	{
          content: "<your_content_to_sign>", // Required
          bip44AddressIndex: 0, // Optional
          bip44change: 0, // Optional
          derivationPath: [44, 0, 0, 0, 0] // Optional
        }
      ]
    },
  },
}

📘

rawMessageData object:

See here for more details on how to construct therawMessageData object

📘

About messages

You can aggregate a maximum of 127 messages for Raw Signing


The example below illustrates the use of the Raw Signing feature for Bitcoin arbitrary message signing:

import { readFileSync } from 'fs';
import { 
  Fireblocks, 
  BasePath, 
  TransactionOperation, 
  TransferPeerPathType, 
  TransactionRequest, 
  TransactionResponse, 
  FireblocksResponse, 
  TransactionStateEnum, 
  CreateTransactionResponse 
} from "@fireblocks/ts-sdk";
import { createHash } from "crypto";

const FIREBLOCKS_API_SECRET_PATH = "<PATH_TO_SECRET>";
const API_KEY = "<API_KEY>"

// Initialize a Fireblocks API instance with local variables
const fireblocks = new Fireblocks({
    apiKey: API_KEY,
    basePath: BasePath.US, // Basepath.Sandbox for the sandbox env
    secretKey: readFileSync(FIREBLOCKS_API_SECRET_PATH, "utf8"),
});


const transactionPayload: TransactionRequest = {
  assetId: "BTC",
  operation: TransactionOperation.Raw,
  source: {
    type: TransferPeerPathType.VaultAccount,
    id: "0",
  },
  note: ``,
  extraParameters: {
    rawMessageData: {},
  },
};

let txInfo: any;

const getTxStatus = async (txId: string): Promise<TransactionResponse> => {
  try {
    let response: FireblocksResponse<TransactionResponse> =
      await fireblocks.transactions.getTransaction({ txId });
    let tx: TransactionResponse = response.data;
    let messageToConsole: string = `Transaction ${tx.id} is currently at status - ${tx.status}`;

    console.log(messageToConsole);
    while (tx.status !== TransactionStateEnum.Completed) {
      await new Promise((resolve) => setTimeout(resolve, 3000));

      response = await fireblocks.transactions.getTransaction({ txId });
      tx = response.data;

      switch (tx.status) {
        case TransactionStateEnum.Blocked:
        case TransactionStateEnum.Cancelled:
        case TransactionStateEnum.Failed:
        case TransactionStateEnum.Rejected:
          throw new Error(
            `Signing request failed/blocked/cancelled: Transaction: ${tx.id} status is ${tx.status}`,
          );
        default:
          console.log(messageToConsole);
          break;
      }
    }
    while (tx.status !== TransactionStateEnum.Completed);
    return tx;
  } catch (error) {
    throw error;
  }
};

const rawSign = async (
  message: string,
): Promise<CreateTransactionResponse | undefined> => {
  const wrappedMessage =
    "\x18Bitcoin Signed Message:\n" +
    String.fromCharCode(message.length) +
    message;
  const hash = createHash("sha256").update(wrappedMessage, "utf8").digest();
  const content = createHash("sha256").update(hash).digest("hex");
  //@ts-ignore
  transactionPayload.extraParameters.rawMessageData = {
    messages: [
      {
        content,
        bip44addressIndex: 0,
      },
    ],
  };
  transactionPayload.note = `BTC Message: ${message}`;
  try {
    const transactionResponse = await fireblocks.transactions.createTransaction(
      {
        transactionRequest: transactionPayload,
      },
    );
    //@ts-ignore
    console.log(transactionPayload.extraParameters.rawMessageData);
    const txId = transactionResponse.data.id;
    if (!txId) {
      throw new Error("Transaction ID is undefined.");
    }
    txInfo = await getTxStatus(txId);
    console.log(JSON.stringify(txInfo, null, 2));
    const signature = txInfo.signedMessages[0].signature;

    console.log(JSON.stringify(signature));

    const encodedSig =
      Buffer.from([Number.parseInt(signature.v, 16) + 31]).toString("hex") +
      signature.fullSig;
    console.log(
      "Encoded Signature:",
      Buffer.from(encodedSig, "hex").toString("base64"),);
    return 
  } catch (error) {
    console.error(error);
  }
};

rawSign("My message23");

import { createHash } from "crypto";
import * as fs from "fs"
import * as path from "path"
import { FireblocksSDK, PeerType, TransactionOperation, TransactionStatus } from "fireblocks-sdk";

const apiKey = "<YOUR_API_KEY>"
const apiSecretPath = "<PATH_TO_SECRET>"
const apiSecret = fs.readFileSync(path.resolve(__dirname, apiSecretPath), "utf8"); 
const fireblocks = new FireblocksSDK(apiSecret, apiKey);

async function signArbitraryMessage(fireblocks: FireblocksSDK, vaultAccountId: string, message: string, bip44addressIndex = 0) {
    const wrappedMessage = "\x18Bitcoin Signed Message:\n" +  String.fromCharCode(message.length) + message;

    const hash = createHash('sha256').update(wrappedMessage, 'utf8').digest();

    const content = createHash('sha256').update(hash).digest("hex");

    const { status, id } = await fireblocks.createTransaction({
        operation: TransactionOperation.RAW,
        assetId: "BTC",
        source: {
            type: PeerType.VAULT_ACCOUNT,
            id: vaultAccountId
        },
        note: `BTC Message: ${message}`,
        extraParameters: {
            rawMessageData: {
                messages: [{
                    content,
                    bip44addressIndex
                }]
            }
        }
    });

    let txInfo;
    let currentStatus = status;
  	while(currentStatus != TransactionStatus.COMPLETED && currentStatus != TransactionStatus.FAILED) {
        try {
            console.log("keep polling for tx " + id + "; status: " + currentStatus);
            txInfo = await fireblocks.getTransactionById(id);
            currentStatus = txInfo.status;
        } catch (err) {
            console.log("err", err);
        }
        await new Promise(r => setTimeout(r, 1000));    
    };
  
    const signature = txInfo.signedMessages[0].signature;

    console.log(JSON.stringify(signature));


    const encodedSig = Buffer.from([signature.v + 31]).toString("hex") + signature.fullSig;
    console.log("Encoded Signature:", Buffer.from(encodedSig,"hex").toString("base64"));
} 
 signArbitraryMessage(fireblocks, "0", "INSERT TEXT HERE");
import binascii
from time import sleep
from hashlib import sha256
from ecdsa.util import sigencode_der
from fireblocks_sdk import TransferPeerPath, FireblocksSDK, RawMessage, UnsignedMessage, VAULT_ACCOUNT, \
    TRANSACTION_STATUS_COMPLETED, TRANSACTION_STATUS_FAILED


def sign_arbitrary_message(vault_id: str, message: str, bip44_address_index: int = 0):
    # Hash the message with sha256 (twice per Bitcoin standard)
    content = sha256(sha256(message.encode("utf-8")).digest()).hexdigest()
    
    # Create an API call to Fireblocks - RAW signing request
    tx_response = fireblocks.create_raw_transaction(
        asset_id="BTC",
        raw_message=RawMessage(
            [UnsignedMessage(content=content, bip44addressIndex=bip44_address_index)]
        ),
        source=TransferPeerPath(VAULT_ACCOUNT, vault_id),
        note=f"BTC Message: {message}",
    )
    
    # Get the transaction's status and id
    tx_status = tx_response["status"]
    tx_id = tx_response["id"]
    
    # Wait for the transaction to complete or to fail
    while tx_status != TRANSACTION_STATUS_COMPLETED and tx_status != TRANSACTION_STATUS_FAILED:
        print(f"Tx {tx_id} is not completed yet. Status: {tx_status}")
        tx_info = fireblocks.get_transaction_by_id(tx_id)
        tx_status = tx_info["status"]
        sleep(2)
    
    # If successfully completed - get the signature and DER encode it
    if tx_status == TRANSACTION_STATUS_COMPLETED:
        signature = tx_info["signedMessages"][0]["signature"]
        r_int = int(signature["r"], 16)
        s_int = int(signature["s"], 16)
        der_encoded_sig = sigencode_der(r_int, s_int, order=0)

        print(f"DER-Encoded HEX signature: {binascii.hexlify(der_encoded_sig).decode()}")
        return
    # Else - failed
    print("Transaction Failed")

if __name__ == '__main__':
    sign_arbitrary_message("0", "my_test_message")
  1. Start by creating a transaction of type RAW, shown under operation (JS) or by using create_raw_transaction() (PY).
  2. Now, specify the source who is signing the message, which is the default vault (Id 0) in our case.

After sending the transaction for signing, wait for its completion so you can retrieve the signature.

The loop in the example above waits for the status of the transaction to be COMPLETED, or until an issue arises.

Once the transaction is complete, we print the full signature by accessing the transaction object signedMessages, then signature.

📘

BTC arbitrary message signing can be also performed by Typed Message signing and it is considered more secure. Check out our Typed Message guide