Typed Message Signing

Prerequisites

Overview

Typed Message is a popular activity within the Ethereum ecosystem. This basically lets you sign any arbitrary message you would like.

  • You can read more about ERC-712 (Typed Structure Data Hashing & Signing) here.
  • You can read more about ERC-191 (Signed Data Standard)here

📘

Typed Message Signing is available for the following assets:

  • Any supported EVM-based network (except for FLR and SGB)
  • Bitcoin personal message

Transaction Authorization Policy (TAP) Requirements

In order to execute Typed Message transactions from Fireblocks, the issuer must be authorized to do so via a "Typed Message" TAP rule, that explicitly allows for typed messages, as shown here:

Typed message signing: EIP-712 example

See below the use of typed message in the EIP-712 standard:

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 chainId = 1 // Update the chainId for the relevant EVM network

const eip712message = {
  "types": {
    "EIP712Domain": [
      {"name": "name", "type": "string"},
      {"name": "version", "type": "string"},
      {"name": "chainId", "type": "uint256"},
      {"name": "verifyingContract", "type": "address"}
    ],
    "Permit": [
      {"name": "holder", "type": "address"},
      {"name": "spender", "type": "address"},
      {"name": "nonce", "type": "uint256"},
      {"name": "expiry", "type": "uint256"},
      {"name": "allowed", "type": "bool"}
    ]
  },
  "primaryType": "Permit",
  "domain": {
    "name": "Dai Stablecoin",
    "version": "1",
    chainId,
    "verifyingContract": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
  },
  "message": {
    "holder": "0x289826f7248b698B2Aef6596681ab0291BFB2599",
    "spender": "0x043f38E9e8359ca32cD57503df25B8DEF2845356",
    "nonce": 123,
    "expiry": 1655467980,
    "allowed": true
  }
}


const transactionPayload = {
  operation: TransactionOperation.TypedMessage,
  assetId: "ETH",
  source: {
    type: TransferPeerPathType.VaultAccount,
    id: "0", // The vault account ID represnting the address used to sign
  },
  note: "Test EIP-712 Message",
  extraParameters: {
    rawMessageData: {
      messages: [
        {
          content: eip712message,
          type: "EIP712"
        }

      ],
    },
  },
};

let txInfo: any;

const signArbitraryMessage = async (): Promise<
  TransactionResponse | undefined
> => {
  try {
    const transactionResponse = await fireblocks.transactions.createTransaction(
      {
        transactionRequest: transactionPayload,
      },
    );
    const txId = transactionResponse.data.id;
    if (!txId) {
      throw new Error("Transaction ID is undefined.");
    }
    txInfo = await getTxStatus(txId);
    console.log(txInfo)
    
    return transactionResponse.data;
    
  } catch (error) {
    console.error(error);
  }
};

signArbitraryMessage();

const chainId = 1; // Update the chainId for the relevant EVM network

const eip712message = {
  "types": {
    "EIP712Domain": [
      {"name": "name", "type": "string"},
      {"name": "version", "type": "string"},
      {"name": "chainId", "type": "uint256"},
      {"name": "verifyingContract", "type": "address"}
    ],
    "Permit": [
      {"name": "holder", "type": "address"},
      {"name": "spender", "type": "address"},
      {"name": "nonce", "type": "uint256"},
      {"name": "expiry", "type": "uint256"},
      {"name": "allowed", "type": "bool"}
    ]
  },
  "primaryType": "Permit",
  "domain": {
    "name": "Dai Stablecoin",
    "version": "1",
    chainId, 
    "verifyingContract": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
  },
  "message": {
    "holder": "0x289826f7248b698B2Aef6596681ab0291BFB2599",
    "spender": "0x043f38E9e8359ca32cD57503df25B8DEF2845356",
    "nonce": 123,
    "expiry": 1655467980,
    "allowed": true
  }
}


async function signEIP712Message(vaultAccountId: string, messageToSign: any) {
  const { status, id } = await fireblocks.createTransaction({
    operation: TransactionOperation.TYPED_MESSAGE,
    assetId: "ETH",
    source: {
      type: PeerType.VAULT_ACCOUNT,
      id: vaultAccountId
    },
    amount: "0",
    note: "Test EIP-712 Message",
    extraParameters: {
      rawMessageData: {
        messages: [
          {
            content: messageToSign,
            type: "EIP712"
          }
        ]
      },
    },
  });
  let currentStatus = status;
  let txInfo;

  while (currentStatus != TransactionStatus.COMPLETED && currentStatus != TransactionStatus.FAILED) {
    console.log("keep polling for tx " + id + "; status: " + currentStatus);
    txInfo = await fireblocks.getTransactionById(id);
    currentStatus = txInfo.status;
    await new Promise(r => setTimeout(r, 1000));
  };
}

signEIP712Message("0", eip712message);



Typed message signing: EIP-191 example

See below the use of typed message signing API for Ethereum message signing.

let txInfo:any;

const transactionPayload = {
  operation: TransactionOperation.TypedMessage,
  assetId: "ETH",
  source: {
    type: TransferPeerPathType.VaultAccount,
    id: "0", // The vault account ID represnting the address used to sign
  },
  note: `Test Message`,
  extraParameters: {
    rawMessageData: {
      messages: [
        {
          content: Buffer.from("INSERT TEXT HERE!!").toString("hex"),
          type: "ETH_MESSAGE",
        },
      ],
    },
  },
};

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 signArbitraryMessage = async (): Promise<
  TransactionResponse | undefined
> => {
  try {
    const transactionResponse = await fireblocks.transactions.createTransaction(
      {
        transactionRequest: transactionPayload,
      },
    );
    const txId = transactionResponse.data.id;
    if (!txId) {
      throw new Error("Transaction ID is undefined.");
    }
    txInfo = await getTxStatus(txId);

    console.log(txInfo)
    return transactionResponse.data;

  } catch (error) {
    console.error(error);
  }
};

signArbitraryMessage();

async function signArbitraryMessage(fireblocks: FireblocksSDK, vaultAccountId: string, message: string, addressIndex = 0) {

  const { status, id } = await fireblocks.createTransaction({
    operation: TransactionOperation.TYPED_MESSAGE,
    assetId: "ETH",
    source: {
      type: PeerType.VAULT_ACCOUNT,
      id: vaultAccountId
    },
    note: `Test Message`,
    extraParameters: {
      rawMessageData: {
        messages: [{
          content: Buffer.from(message).toString("hex"),
          type: "ETH_MESSAGE"
        }]
      }
    }
  });
  let currentStatus = status;
  let txInfo: any;

  while (currentStatus != TransactionStatus.COMPLETED && currentStatus != TransactionStatus.FAILED) {
    console.log("keep polling for tx " + id + "; status: " + currentStatus);
    txInfo = await fireblocks.getTransactionById(id);
    currentStatus = txInfo.status;
    await new Promise(r => setTimeout(r, 1000));
  };
}

signArbitraryMessage(fireblocks, "0", "INSERT TEXT HERE");

📘

Working with EVM Based Networks:

Since all assets on EVM-based networks share the same address as the ETH wallet within the same vault, you should always specify assetId = ETH when initiating Typed Message Signing.

Structuring The Signature:

Once the Typed Message Signing request is successfully signed, you can retrieve the transaction using the Get Transaction By TxId Endpoint . The signed transaction will appear in the following format:

{
  "id": "<your_transaction_id>",
  "assetId": "ETH",
  "source": {
    "id": "<vault_account_id>",
    "type": "VAULT_ACCOUNT",
    "name": "<vault_account_name>",
    "subType": ""
  },
  "destination": {
    "id": null,
    "type": "UNKNOWN",
    "name": "N/A",
    "subType": ""
  },
  "requestedAmount": null,
  "amount": null,
  "netAmount": -1,
  "amountUSD": null,
  "fee": -1,
  "networkFee": -1,
  "createdAt": 1712520602396,
  "lastUpdated": 1712520622523,
  "status": "COMPLETED",
  "txHash": "",
  "subStatus": "",
  "sourceAddress": "",
  "destinationAddress": "",
  "destinationAddressDescription": "",
  "destinationTag": "",
  "signedBy": [],
  "createdBy": "<API_KEY>",
  "rejectedBy": "",
  "addressType": "",
  "note": "Test EIP-712 Message",
  "exchangeTxId": "",
  "feeCurrency": "ETH",
  "operation": "TYPED_MESSAGE",
  "amountInfo": {},
  "feeInfo": {},
  "signedMessages": [
    {
      "derivationPath": [
        44,
        60,
        0,
        0,
        0
      ],
      "algorithm": "MPC_ECDSA_SECP256K1",
      "publicKey": "03af66c4551559d54bfbfd14c84870a337b06bf2738ed6427480ec56ee551c7458",
      "signature": {
        "r": "2e31d257c1bcd232c50d628e9e97407373c4a1c5cc79672039a1f7946984a702",
        "s": "370b8e16123e30968ba7018a6726f97dfc82f5547f99fe78b432a40a1d1f8564",
        "v": 0,
        "fullSig": "2e31d257c1bcd232c50d628e9e97407373c4a1c5cc79672039a1f7946984a702370b8e16123e30968ba7018a6726f97dfc82f5547f99fe78b432a40a1d1f8564"
      },
      "content": "36c0b1b40bcd032c871ca176243f5ff7e603a9ce91ff8dae62d79ab8dee6817a"
    }
  ],
  "extraParameters": {
    "rawMessageData": {
      "messages": [
        {
          "type": "EIP712",
          "index": 0,
          "content": {
            "types": {
              "Permit": [
                {
                  "name": "holder",
                  "type": "address"
                },
                {
                  "name": "spender",
                  "type": "address"
                },
                {
                  "name": "nonce",
                  "type": "uint256"
                },
                {
                  "name": "expiry",
                  "type": "uint256"
                },
                {
                  "name": "allowed",
                  "type": "bool"
                }
              ],
              "EIP712Domain": [
                {
                  "name": "name",
                  "type": "string"
                },
                {
                  "name": "version",
                  "type": "string"
                },
                {
                  "name": "chainId",
                  "type": "uint256"
                },
                {
                  "name": "verifyingContract",
                  "type": "address"
                }
              ]
            },
            "domain": {
              "name": "Dai Stablecoin",
              "chainId": 1,
              "version": "1",
              "verifyingContract": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
            },
            "message": {
              "nonce": 123,
              "expiry": 1655467980,
              "holder": "0x289826f7248b698B2Aef6596681ab0291BFB2599",
              "allowed": true,
              "spender": "0x043f38E9e8359ca32cD57503df25B8DEF2845356"
            },
            "primaryType": "Permit"
          }
        }
      ]
    }
  },
  "destinations": [],
  "blockInfo": {},
  "assetType": "BASE_ASSET"
}
{
  "id": "<your_transaction_id>",
  "assetId": "ETH",
  "source": {
    "id": "<vault_account_id>",
    "type": "VAULT_ACCOUNT",
    "name": "<vault_account_name>",
    "subType": ""
  },
  "destination": {
    "id": null,
    "type": "UNKNOWN",
    "name": "N/A",
    "subType": ""
  },
  "requestedAmount": null,
  "amount": null,
  "netAmount": -1,
  "amountUSD": null,
  "fee": -1,
  "networkFee": -1,
  "createdAt": 1712522541024,
  "lastUpdated": 1712522564793,
  "status": "COMPLETED",
  "txHash": "",
  "subStatus": "",
  "sourceAddress": "",
  "destinationAddress": "",
  "destinationAddressDescription": "",
  "destinationTag": "",
  "signedBy": [],
  "createdBy": "<API_KEY>",
  "rejectedBy": "",
  "addressType": "",
  "note": "Test EIP191 Message",
  "exchangeTxId": "",
  "feeCurrency": "ETH",
  "operation": "TYPED_MESSAGE",
  "amountInfo": {},
  "feeInfo": {},
  "signedMessages": [
    {
      "derivationPath": [
        44,
        60,
        0,
        0,
        0
      ],
      "algorithm": "MPC_ECDSA_SECP256K1",
      "publicKey": "03af66c4551559d54bfbfd14c84870a337b06bf2738ed6427480ec56ee551c7458",
      "signature": {
        "r": "c8ab06c7c3447ac8f594a643d5942ceb2451b9434bb29c4b1a40da5cf3240300",
        "s": "2bfba73d825b240f2e2949df139c2f3e26e6cdd36579eb1a0cbd4abc4e6e348a",
        "v": 0,
        "fullSig": "c8ab06c7c3447ac8f594a643d5942ceb2451b9434bb29c4b1a40da5cf32403002bfba73d825b240f2e2949df139c2f3e26e6cdd36579eb1a0cbd4abc4e6e348a"
      },
      "content": "90089ed244695164981f5f54e78bea15387a2bdda0dca6a81e1fe79cd30075db"
    }
  ],
  "extraParameters": {
    "rawMessageData": {
      "messages": [
        {
          "type": "ETH_MESSAGE",
          "index": 0,
          "content": "494e53455254205445585420484552452121"
        }
      ]
    }
  },
  "destinations": [],
  "blockInfo": {},
  "assetType": "BASE_ASSET"
}

To access the signature, navigate to the object at the first index (0) of the signedMessages array. Within this object, the signature object contains three components: r, s, and v:

  • r: This is the first 32 bytes of the signature, representing the X coordinate of the point on the elliptic curve
  • s: This is the second 32 bytes of the signature, representing the scalar component of the elliptic curve point
  • v: This parameter is crucial for correctly reconstructing the public key used in signing. It helps distinguish between the possible public keys that could correspond to the signature

There are principally two methods to assemble a complete, correctly structured signature on EVM-based blockchains, differing primarily in how the v value is calculated, a modification introduced by EIP-155 .
EIP-155 prevents certain types of replay attacks by incorporating the chainId into the v value:

const signature = txInfo.signedMessages[0].signature;
const v = 27 + signature.v;

const finalSignature =  "0x" + signature.r + signature.s + v.toString(16);
const signature = txInfo.signedMessages[0].signature;
const v = chainId * 2 + 35 + signature.v;

const finalSignature = "0x" + signature.r + signature.s + v.toString(16);

It is important to consider the chainId value when working with any EVM-based blockchain as it directly affects the calculation of the v parameter.

Properly calculating v ensures that the signature correctly corresponds to the network on which the transaction is intended, thus preventing cross-chain replay attacks.

A full list of EVM Networks chainId values can be found here .


Bitcoin personal message example

const transactionPayload = {
  operation: TransactionOperation.TypedMessage,
  assetId: "BTC",
  source: {
    type: TransferPeerPathType.VaultAccount,
    id: "0", // The vault account ID represnting the address used to sign
  },
  note: "Bitcoin Message",
  extraParameters: {
    rawMessageData: {
      messages: [
        {
          content: "", // Content remains blank and is replaced by the message built when signing the TX
          type: "BTC_MESSAGE",
        },
      ],
    },
  },
};
const getWalletAddress = async (): Promise<VaultWalletAddress | any> => {
  try {
    const addressResponse =
      await fireblocks.vaults.getVaultAccountAssetAddressesPaginated({
        vaultAccountId: transactionPayload.source.id,
        assetId: transactionPayload.assetId,
      });
    if (addressResponse.data.addresses.length === 0) {
      throw new Error("No wallet addresses found");
    }
    return addressResponse.data;
  } catch (error) {
    console.error(error);
  }
};

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 signArbitraryMessage = async (): Promise<
  TransactionResponse | undefined
> => {
  const address = await getWalletAddress();
  try {
    // replacing payload message with the format required before creating the transaction
    // message format is {vasp_prefix}{address}{UUID}
    const message =
      "tripleaio" + address + "975f0090-a88f-4be0-a123-d38484e8394d";
    transactionPayload.extraParameters.rawMessageData.messages[0].content =
      message;

    const transactionResponse = await fireblocks.transactions.createTransaction(
      {
        transactionRequest: transactionPayload,
      },
    );
    const txId = transactionResponse.data.id;
    if (!txId) {
      throw new Error("Transaction ID is undefined.");
    }
    txInfo = await getTxStatus(txId);
    const signature = txInfo.signedMessages[0].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 transactionResponse.data;
  } catch (error) {
    console.error(error);
  }
};

signArbitraryMessage();

const fbClient = require('./fb_client');
const {TransactionOperation, PeerType, TransactionStatus} = require("fireblocks-sdk");
const util = require("util");

/*
* Create a transaction with a typed message
* The message format is {vasp_prefix}{address}{UUID}
* vasp_prefix: an arbitrary string when the receiving VASP claimed custody of an
* address (stored in BB)
* address: the address of the wallet
* UUID: the address registration UUID when the receiving VASP claimed custody of an address
*
* docs: https://developers.fireblocks.com/docs/typed-message-signing
*/
async function signArbitraryMessage(vaultAccountId, message, asset, addressIndex = 0) {

    const { status, id } = await fbClient.createTransaction({
        operation: TransactionOperation.TYPED_MESSAGE,
        assetId: asset,
        source: {
            type: PeerType.VAULT_ACCOUNT,
            id: vaultAccountId
        },
        note: `Test Message`,
        extraParameters: {
            rawMessageData: {
                messages: [{
                    content: message,
                    bip44AddressIndex: addressIndex,
                    type: "BTC_MESSAGE"
                }]
            }
        }
    });
    let currentStatus = status;
    let txInfo;

    if(currentStatus === TransactionStatus.COMPLETED) {
        console.log("Transaction Status: ", currentStatus);
        txInfo = await fbClient.getTransactionById(id);
    }

    while (currentStatus !== TransactionStatus.COMPLETED && currentStatus !== TransactionStatus.FAILED) {
        console.log("keep polling for tx " + id + "; status: " + currentStatus);
        txInfo = await fbClient.getTransactionById(id);
        currentStatus = txInfo.status;
        await new Promise(r => setTimeout(r, 1000));
    };

    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"));
}

(async () => {
    const vaultAccountId = 0;
    const asset = "BTC_TEST";
    const walletAddresses = await fbClient.getDepositAddresses(vaultAccountId, asset);

    console.log("PUB_____KEY_______: ", walletAddresses);

    if (walletAddresses.length === 0) {
        throw new Error("No wallet addresses found");
    }
    // message format is {vasp_prefix}{address}{UUID}
    const message = "tripleaio" + walletAddresses[0].address + "975f0090-a88f-4be0-a123-d38484e8394d";
    await signArbitraryMessage("0", message, asset);
})();