Skip to main content
Migrate an existing Fireblocks Non-Custodial Wallet (NCW) into a Dynamic WaaS wallet. Migration takes over the Fireblocks key material and provisions WaaS wallet accounts for every supported chain in a single cross-chain call, preserving the addresses the user already had. Before this: create and initialize a Dynamic client (see Creating a Dynamic Client, Initializing the Dynamic Client) and add a WaaS extension for at least one chain.
Before you start. This is for apps already running a Fireblocks NCW integration. The user must be authenticated with Dynamic — migration creates the WaaS wallet for the current user, and your Fireblocks wallet is left untouched. You supply the existing device’s deviceId, password, and storage from your NCW integration, plus a jwt from your own auth. Migration currently supports EVM and Solana; other chains come back as a per-chain failure (see Result).

Prerequisites

Fireblocks migration is behind a feature flag. Contact support to enable it for your environment.
Set up the migration backend before users migrate — see Migrate from Fireblocks for the full setup. In short:
  • Dashboard — configure the migration backend base URL and environment (sandbox / production) in your environment settings.
  • Backend — expose POST /api/devices/{deviceId}/rpc that relays NCW messages to Fireblocks, and enable CORS for the Dynamic iframe origin https://app.dynamicauth.com.
Fireblocks migration settings in the Dynamic dashboard

Usage

import { migrateFromFireblocks } from '@dynamic-labs-sdk/client/waas';

const { wallets } = await migrateFromFireblocks({
  deviceId,   // Fireblocks NCW device id
  jwt,        // your app's auth token for the user; forwarded to your backend's /rpc
  password,   // password that encrypted the NCW device storage
  storage,    // the device's NCW-* entries, verbatim as ciphertext (see "Building the storage map")
});

const failed = wallets.filter((w) => !w.result.ok);

Parameters

  • deviceId: string — The Fireblocks NCW device id being migrated.
  • jwt: string — Your application’s auth token for the user (for example, from social login). It’s forwarded as the bearer token to your backend’s RPC endpoint, which authenticates the request. This is not a Fireblocks credential — your Fireblocks API auth stays on the backend.
  • password: string — The password that encrypted the NCW device storage. Required.
  • storage: Record<string, string> — The device’s persisted Fireblocks NCW entries (every NCW-* key), verbatim — the encrypted values exactly as stored. See Building the storage map.
  • expectedAddresses: Record<string, string> — Required. The wallet address Fireblocks held per chain name (for example, { EVM: "0x…" }). Its keys are the chains migration runs on — only those chains are migrated — and each imported Dynamic address is verified against the value, so a wrong derivation fails loudly instead of producing an unusable wallet.
Pass a client as the second argument only when using multiple Dynamic clients.

Building the storage map

storage must contain the device’s persisted Fireblocks NCW entries (every NCW-* key) verbatim — the encrypted values exactly as stored. The migration iframe re-hydrates this map and decrypts the secure entries itself using the same password and deviceId, so the values must stay as ciphertext. Build it in your application, where your Fireblocks NCW integration persisted the device. The snippet below reads the default browser storage; if you configured a custom NCW storage provider, read the device’s entries from there instead.
import { BrowserLocalStorageProvider } from "@fireblocks/ncw-js-sdk";

const buildStorage = async (): Promise<Record<string, string>> => {
  const provider = new BrowserLocalStorageProvider();
  // getAllKeys() returns logical keys with the `NCW-` prefix stripped.
  const keys = await provider.getAllKeys();
  const storage: Record<string, string> = {};
  for (const key of keys) {
    // The base provider returns the raw stored value (ciphertext for secure entries).
    const value = await provider.get(key);
    // Re-add the `NCW-` prefix so the map matches what was persisted.
    if (value !== null) storage[`NCW-${key}`] = value;
  }
  return storage;
};
Enumerate the base BrowserLocalStorageProvider so secure entries stay encrypted. Do not read entries through a secure-storage provider (ISecureStorageProvider, e.g. PasswordEncryptedLocalStorage) — its get() decrypts to plaintext, which will not round-trip through the takeover.

Result

type MigrateFromFireblocksResponse = {
  wallets: MigratedWalletResult[];
  migration: { fireblocksDeviceId: string };
};

type MigratedWalletResult = {
  chainName: string;
  result:
    // Migrated on this run.
    | { ok: true; account: CreateWalletAccountResponse; fireblocksAlgorithm: string; fireblocksKeyId: string }
    // Already migrated on a prior run — the wallet exists, nothing to do.
    | { ok: true; alreadyMigrated: true }
    // This chain failed; other chains may still have succeeded.
    | { ok: false; errorCode: string };
};
Migration is per chain, so inspect wallets to see which chains migrated. account is the created WaaS wallet account; fireblocksAlgorithm and fireblocksKeyId identify the source Fireblocks key, useful for logging and reconciliation. Re-running migration is safe: a chain already imported on a prior run comes back as { ok: true, alreadyMigrated: true }. A { ok: false } entry means that chain failed while others may have succeeded — surface it and let the user retry. Possible errorCode values:
errorCodeMeaning
MIGRATION_UNSUPPORTED_CHAINThe chain can’t be migrated from a Fireblocks NCW wallet (e.g. Aleo, Midnight).
MIGRATION_UNVERIFIED_CHAINThe chain isn’t supported for migration yet.
MIGRATION_NO_KEY_FOR_ALGOThe device has no key for that chain’s signing algorithm.
MIGRATION_IMPORT_*The key import into Dynamic failed (transient backend or address-check issue). Safe to retry.
Treat any other ok: false as a failure to surface and retry.

Error Handling

These are thrown from the call itself (distinct from the per-chain errorCode above):
  • If no WaaS wallet provider is registered, it throws a NoWalletProviderFoundError.
  • If the resolved provider is not a WaaS provider, it throws a NotWaasWalletProviderError.
  • A wrong password fails the takeover; it does not create a usable wallet.
Last modified on June 25, 2026