Skip to main content
This guide shows how to sign and submit RebelFi transactions using Fireblocks as your custody provider. Fireblocks is a common choice for exchanges, funds, and institutions that need policy-based signing and MPC key management.
For Fireblocks setup, API keys, and vault configuration, see the Fireblocks Developer Docs.

How It Works

1. Plan operation        →  RebelFi returns unsigned EVM transaction(s)
2. Create Fireblocks tx  →  Pass calldata to Fireblocks CONTRACT_CALL
3. Fireblocks signs      →  MPC signing + policy engine approval
4. Poll for completion   →  Wait for Fireblocks to broadcast and confirm
5. Submit hash           →  Report the on-chain txHash back to RebelFi
RebelFi returns unsigned transactions with to, data, and gas parameters. You pass the to address and data to Fireblocks as a CONTRACT_CALL, and Fireblocks handles signing, gas estimation, and broadcasting.

Prerequisites

  • A Fireblocks workspace with API access
  • A vault account with an EVM asset (e.g., ETH_TEST5 for testnet, ETH for mainnet)
  • The vault’s deposit address registered as a wallet in RebelFi
  • fireblocks-sdk installed: npm install fireblocks-sdk

Vault-to-Wallet Mapping

Your Fireblocks vault account has a deposit address for each asset. This address is what you register with RebelFi:
import { FireblocksSDK } from 'fireblocks-sdk';

const fireblocks = new FireblocksSDK(privateKey, apiKey, apiBaseUrl);

// Get the vault's deposit address for an EVM asset
const addresses = await fireblocks.getDepositAddresses(vaultAccountId, assetId);
const walletAddress = addresses[0].address;

// Register this address with RebelFi
const wallet = await rebelfi.wallets.register({
  walletAddress,
  blockchain: 'ethereum', // or 'polygon', etc.
});
The wallet address you register with RebelFi must match the Fireblocks vault’s deposit address for that asset. If they don’t match, transaction signing will fail.

The Fireblocks Signer

The core of the integration is a function that takes RebelFi’s unsigned transaction fields and submits them through Fireblocks:
import fs from 'node:fs';
import {
  FireblocksSDK,
  PeerType,
  TransactionOperation,
  TransactionStatus,
} from 'fireblocks-sdk';
import type { EvmTransactionFields } from '@rebelfi/sdk';

const POLL_INTERVAL_MS = 3_000;
const MAX_POLL_ATTEMPTS = 120; // 6 minutes max

const TERMINAL_STATUSES = new Set([
  TransactionStatus.COMPLETED,
  TransactionStatus.CONFIRMED,
  TransactionStatus.FAILED,
  TransactionStatus.CANCELLED,
  TransactionStatus.REJECTED,
  TransactionStatus.BLOCKED,
  TransactionStatus.TIMEOUT,
]);

const fireblocks = new FireblocksSDK(
  fs.readFileSync(process.env.FIREBLOCKS_SECRET_KEY_PATH, 'utf-8'),
  process.env.FIREBLOCKS_API_KEY,
  process.env.FIREBLOCKS_API_BASE_URL
);

async function signAndBroadcast(
  evmTx: EvmTransactionFields,
  vaultAccountId: string,
): Promise<{ txHash: string }> {
  // Create a CONTRACT_CALL transaction in Fireblocks
  const { id: fbTxId } = await fireblocks.createTransaction({
    operation: TransactionOperation.CONTRACT_CALL,
    assetId: process.env.FIREBLOCKS_ASSET_ID, // e.g., 'ETH' or 'ETH_TEST5'
    source: {
      type: PeerType.VAULT_ACCOUNT,
      id: vaultAccountId,
    },
    destination: {
      type: PeerType.ONE_TIME_ADDRESS,
      oneTimeAddress: { address: evmTx.to },
    },
    amount: '0',
    extraParameters: {
      contractCallData: evmTx.data,
    },
    note: 'RebelFi operation',
  });

  // Poll until Fireblocks signs and broadcasts
  for (let i = 0; i < MAX_POLL_ATTEMPTS; i++) {
    await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
    const txInfo = await fireblocks.getTransactionById(fbTxId);

    if (!TERMINAL_STATUSES.has(txInfo.status)) continue;

    if (
      txInfo.status === TransactionStatus.COMPLETED ||
      txInfo.status === TransactionStatus.CONFIRMED
    ) {
      return { txHash: txInfo.txHash };
    }

    throw new Error(
      `Fireblocks transaction ${fbTxId} ended with status: ${txInfo.status}`
    );
  }

  throw new Error(`Fireblocks transaction ${fbTxId} timed out`);
}
Fireblocks handles gas estimation and broadcasting. You pass amount: '0' because the contract call itself doesn’t transfer native tokens — it interacts with an ERC-20 contract.

Supply Example

EVM supply operations produce two transactions: a token approval and the protocol deposit. Sign and submit them sequentially through Fireblocks:
import { RebelfiClient } from '@rebelfi/sdk';

const rebelfi = new RebelfiClient({ apiKey: process.env.REBELFI_API_KEY });

async function supply(
  walletId: number,
  strategyId: number,
  amount: string,
  tokenAddress: string,
  vaultAccountId: string,
) {
  // 1. Plan the supply — RebelFi returns unsigned transactions
  const operation = await rebelfi.operations.supply({
    walletId,
    strategyId,
    amount,
    tokenAddress,
  });
  console.log(`Operation ${operation.operationId} created`);

  // 2. Fetch the unsigned transactions
  const unsignedTxs = await rebelfi.operations.getUnsignedTransactions(
    operation.operationId,
  );
  console.log(`${unsignedTxs.length} transaction(s) to sign`);

  // 3. Sign and submit each transaction via Fireblocks
  for (const tx of unsignedTxs) {
    console.log(`Signing: ${tx.description}`);
    const { txHash } = await signAndBroadcast(
      tx.evmTransaction,
      vaultAccountId,
    );

    // 4. Report the hash back to RebelFi
    await rebelfi.transactions.submitHash({
      operationId: operation.operationId,
      txHash,
      transactionId: tx.attemptId,
    });
    console.log(`Submitted: ${tx.description} (${txHash})`);
  }

  // 5. Confirm final status
  const final = await rebelfi.operations.get(operation.operationId);
  console.log(`Final status: ${final.status}`);
  return final;
}

What Happens Step by Step

1

Plan the supply

Call operations.supply() with your wallet, strategy, amount, and token address. RebelFi returns an operation with status AWAITING_SIGNATURE.
2

Fetch unsigned transactions

Call operations.getUnsignedTransactions() to get fresh unsigned transactions. For EVM supply, you’ll get two:
  • Transaction 1: Token approval (approve call on the ERC-20 contract)
  • Transaction 2: Supply to protocol (deposit into the yield strategy)
3

Sign via Fireblocks

For each unsigned transaction, create a Fireblocks CONTRACT_CALL with the to address and data field. Fireblocks signs with MPC, applies your policy rules, and broadcasts on-chain.
4

Submit hash to RebelFi

Once Fireblocks confirms the transaction, take the txHash and submit it to RebelFi via transactions.submitHash(). Include the transactionId to identify which transaction in the operation you’re submitting.
5

Confirm

RebelFi monitors the chain and updates the operation status to CONFIRMED once all transactions land.

Unwind Example

Unwinding (withdrawing from a yield strategy) follows the same pattern:
async function unwind(
  walletId: number,
  strategyId: number,
  amount: string,
  vaultAccountId: string,
) {
  const operation = await rebelfi.operations.unwind({
    walletId,
    strategyId,
    amount,
  });

  const unsignedTxs = await rebelfi.operations.getUnsignedTransactions(
    operation.operationId,
  );

  for (const tx of unsignedTxs) {
    const { txHash } = await signAndBroadcast(
      tx.evmTransaction,
      vaultAccountId,
    );

    await rebelfi.transactions.submitHash({
      operationId: operation.operationId,
      txHash,
      transactionId: tx.attemptId,
    });
  }

  return rebelfi.operations.get(operation.operationId);
}
For a full withdrawal, pass fullWithdrawal: true instead of amount:
const operation = await rebelfi.operations.unwind({
  walletId,
  strategyId,
  fullWithdrawal: true,
});
For sandbox/testnet, use https://sandbox-api.fireblocks.io as the base URL and a testnet asset ID like ETH_TEST5.

Troubleshooting

Cause: The EVM asset hasn’t been added to your Fireblocks vault account.Solution: In the Fireblocks console, go to your vault account and add the asset (e.g., ETH, ETH_TEST5). Then fetch the deposit address.
Cause: Fireblocks policy engine rejected the transaction.Solution: Check your Transaction Authorization Policy (TAP) rules in the Fireblocks console. The CONTRACT_CALL to a ONE_TIME_ADDRESS may need explicit approval or a policy rule allowing it.
Cause: Fireblocks transaction took too long to reach a terminal status.Solution: Check the transaction status in the Fireblocks console. It may be pending human approval (if your policy requires it). Increase MAX_POLL_ATTEMPTS if your approval flow takes longer.
Cause: The vault deposit address doesn’t match the wallet registered with RebelFi.Solution: Use fireblocks.getDepositAddresses(vaultAccountId, assetId) to get the correct address, and ensure that’s the address registered with RebelFi.

Resources