Skip to main content
This guide shows how to sign and submit RebelFi transactions using Dfns as your custody provider. Dfns provides programmable wallets with server-side MPC signing, supporting both EVM and Solana chains.
For Dfns setup, service accounts, and wallet configuration, see the Dfns Developer Docs.

How It Works

1. Plan operation        →  RebelFi returns unsigned transaction(s)
2. Broadcast via Dfns    →  Pass transaction to Dfns broadcastTransaction
3. Dfns signs & sends    →  Server-side MPC signing + on-chain broadcast
4. Poll for confirmation →  Wait for Dfns to confirm the transaction
5. Submit hash           →  Report the on-chain txHash back to RebelFi
Unlike custody providers that require you to build and serialize transactions locally, Dfns handles signing server-side. For EVM, you pass the transaction fields (to, data, value) as a JSON object. For Solana, you pass the serialized unsigned transaction as a hex-encoded string.

Prerequisites

  • A Dfns account with a service account configured
  • An Ed25519 key pair for the service account (generated during setup)
  • A Dfns wallet on the target network (e.g., Ethereum, Solana, Polygon)
  • The wallet address registered with RebelFi
  • @dfns/sdk and @dfns/sdk-keysigner installed: npm install @dfns/sdk @dfns/sdk-keysigner

Setting Up the Dfns Client

Create a Dfns API client using your service account credentials:
import fs from 'node:fs';
import { DfnsApiClient } from '@dfns/sdk';
import { AsymmetricKeySigner } from '@dfns/sdk-keysigner';

const signer = new AsymmetricKeySigner({
  credId: process.env.DFNS_CRED_ID,
  privateKey: fs.readFileSync(process.env.DFNS_PRIVATE_KEY_PATH, 'utf-8'),
});

const dfns = new DfnsApiClient({
  orgId: process.env.DFNS_ORG_ID,
  authToken: process.env.DFNS_AUTH_TOKEN,
  baseUrl: 'https://api.dfns.io',
  signer,
});

Registering a Wallet

Fetch your Dfns wallet address and register it with RebelFi:
import { RebelfiClient, Blockchain } from '@rebelfi/sdk';

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

// Get the wallet address from Dfns
const dfnsWallet = await dfns.wallets.getWallet({
  walletId: process.env.DFNS_WALLET_ID,
});

// Register with RebelFi — detect blockchain from the network
const blockchain = dfnsWallet.network.toLowerCase().includes('solana')
  ? Blockchain.SOLANA
  : Blockchain.ETHEREUM;

const wallet = await rebelfi.wallets.register({
  walletAddress: dfnsWallet.address,
  blockchain,
});
The wallet address you register with RebelFi must match the Dfns wallet address. Each Dfns wallet has a single address tied to its network.

The Dfns Signer

The core of the integration is a function that takes a RebelFi unsigned transaction and broadcasts it through Dfns. It handles both EVM and Solana transactions:
import type { UnsignedTransactionDetail, EvmTransactionFields } from '@rebelfi/sdk';

const POLL_INTERVAL_MS = 3_000;
const MAX_POLL_ATTEMPTS = 120;

const TERMINAL_STATUSES = new Set(['Confirmed', 'Failed', 'Rejected']);

function buildBroadcastBody(tx: UnsignedTransactionDetail) {
  const blockchain = tx.blockchain.toLowerCase();

  if (blockchain === 'solana') {
    // Solana: convert base64 serialized tx to hex
    const hexTx = '0x' + Buffer.from(
      tx.unsignedTransaction.serialized, 'base64'
    ).toString('hex');
    return { kind: 'Transaction' as const, transaction: hexTx };
  }

  // EVM: pass structured transaction fields
  const evm = tx.evmTransaction!;
  return {
    kind: 'Transaction' as const,
    transaction: {
      to: evm.to,
      data: evm.data,
      value: evm.value || '0',
    },
  };
}

async function signAndBroadcast(
  tx: UnsignedTransactionDetail,
  walletId: string,
): Promise<{ txHash: string }> {
  const result = await dfns.wallets.broadcastTransaction({
    walletId,
    body: buildBroadcastBody(tx),
  });

  const dfnsTxId = result.id;

  // Poll until Dfns confirms the transaction on-chain
  for (let i = 0; i < MAX_POLL_ATTEMPTS; i++) {
    await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
    const txInfo = await dfns.wallets.getTransaction({
      walletId,
      transactionId: dfnsTxId,
    });

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

    if (txInfo.status === 'Confirmed') {
      return { txHash: txInfo.txHash! };
    }

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

  throw new Error(`Dfns transaction ${dfnsTxId} timed out`);
}
Dfns handles gas estimation and broadcasting automatically. For EVM transactions, you only need to provide to, data, and value. For Solana, you pass the complete serialized transaction and Dfns adds the signature.

Supply Example

Supply operations may produce multiple transactions (e.g., EVM token approval + deposit, or a single Solana transaction). Sign and submit them sequentially through Dfns:
async function supply(
  walletId: number,
  strategyId: number,
  amount: string,
  tokenAddress: string,
  dfnsWalletId: 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 Dfns
  for (const tx of unsignedTxs) {
    console.log(`Signing: ${tx.description} (${tx.blockchain})`);
    const { txHash } = await signAndBroadcast(tx, dfnsWalletId);

    // 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 the transactions to sign. For EVM, you’ll typically get two (approval + deposit). For Solana, you’ll get one.
3

Broadcast via Dfns

For each unsigned transaction, call broadcastTransaction on the Dfns wallet. For EVM, pass the to, data, and value fields. For Solana, pass the hex-encoded serialized transaction. Dfns signs with MPC and broadcasts on-chain.
4

Submit hash to RebelFi

Once Dfns 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,
  dfnsWalletId: 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, dfnsWalletId);

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

EVM vs Solana

The signAndBroadcast function handles both chains transparently. The key difference is how the transaction is passed to Dfns:
EVMSolana
Transaction formatJSON object (to, data, value)Hex-encoded serialized transaction
Source fieldtx.evmTransactiontx.unsignedTransaction.serialized
Typical tx count2 (approve + deposit)1
Gas handlingDfns estimates automaticallyIncluded in serialized tx
The tx.blockchain field on each unsigned transaction tells you which chain it targets. Use this to route to the correct broadcast format.

Troubleshooting

Cause: A Solana transaction was sent as a JSON object (EVM format) instead of a hex string, or the hex encoding is missing the 0x prefix.Solution: Ensure Solana transactions are converted from base64 to hex with a 0x prefix: '0x' + Buffer.from(serialized, 'base64').toString('hex').
Cause: Dfns policy engine rejected the transaction, or the transaction failed on-chain.Solution: Check the reason field on the Dfns transaction response. Common causes: insufficient gas funds in the wallet, policy rules blocking the transaction, or a smart contract revert.
Cause: Dfns transaction took too long to reach a terminal status.Solution: Check the transaction status in the Dfns dashboard. It may be pending policy approval. Increase MAX_POLL_ATTEMPTS if your approval flow takes longer.
Cause: The Dfns wallet network doesn’t match the RebelFi strategy’s blockchain.Solution: Ensure your WALLET_ID and STRATEGY_ID in RebelFi correspond to the same blockchain as your Dfns wallet. A Solana Dfns wallet needs a Solana-registered RebelFi wallet and a Solana strategy.

Resources