Skip to main content
Version: IOTA

Chrysalis Migration

By the end of the Chrysalis migration, several aspects at IOTA changed for the better. With easier ways to manage and secure your experience, IOTA seamlessly integrated these innovations with no service interruptions.

With Chrysalis, we made a clear-cut from the current IOTA protocol, and started a new with a much better, and more mature network. The network supports many new use cases and created a foundation for IOTA’s upcoming Coordicide.

This included one of the innovations that directly impacted one of the most crucial aspects of your experience: funds.

Token Migration Overview​

Below is an overview of how the migration is taking place for normal token holders:

  1. You enter your seed in Firefly.
  2. Firefly creates a new seed for you and generates an EdDSA address for the new network.
  3. Firefly sends your funds to a specific migration address (which encapsulates your EdDSA address) on the old network.
  4. Your funds become available on the new network on the EdDSA address Firefly created for you.
  5. Your funds are successfully migrated.

If you migrate after the Chrysalis launch, your funds will become available shortly after you migrate (within less than 5 minutes).


Firefly will initially only be available on desktop operating systems such as: macOS, Linux, or Windows and will not support migrations for Ledger devices or using a Ledger device.

For further information on the migration process, see our blog post.

For a detailed explanation on how the migration process works technically, see the migration-mechanism page.

Exchange Token Migration Guide​

To help you successfully transfer your tokens securely to the Chrysalis network, we created this guide as an overview of the migration process and its intricacies.


On the 28th of April 2021, Chrysalis Phase 2 was released (with its corresponding node software, libraries, and tooling). After this, the legacy network only supports migration transfers moving further (this is accompanied by a legacy node release). This means that both a legacy (albeit only for migrations), and a new Chrysalis Phase 2 network co-exist.

There are two ways with which you can migrate your funds from the old legacy to the Chrysalis Phase 2 network:

  1. You can either use our Firefly wallet (which allows migrating from either an 81-tryte seed or seed vault file) (check out this blog post on how to do this).
  2. Or you can craft a migration bundle yourself which transfers your funds to a special migration address under your control (programmatic approach).

This guide will further explain how to create a migration bundle and the rules imposed on it.

Migrating Funds by Issuing Migration Bundles​

As mentioned above, there was a special release for the legacy node software on the 28/4/2021, which will only further support migration bundles. In case you are operating a node yourself you must upgrade to that version, as otherwise you will no longer be synchronized with the network.

Migration bundle​

With this limited legacy network, only migration bundles will be confirmed moving forward. A migration bundle is simply a normal value bundle/transfer which has some additional restrictions. If you craft a bundle which obeys the following rules, then it falls under what we define as a migration bundle:

  • It contains exactly one output transaction of which the destination address is a valid migration address and is positioned as the tail transaction within the bundle (meaning currentIndex 0). The output transaction value is at least 1'000'000 tokens (1 Mi).
  • It does not contain any zero-value transactions which do not hold signature fragments. This means that transactions other than the tail transaction must always be part of an input.
  • Input transactions must not use migration addresses.

If you are in doubt whether your bundle is an actual migration bundle, you can use the ValidBundle(bundle, true) function of our iota.go library to validate this. If you are not acquainted with Go, you can also contact us on Discord or Slack to ensure that your crafted bundles are valid migration bundles.

Here are a few things to consider:

  • You must not broadcast your own migration bundles unless you are 100% sure that they are valid migration bundles.
  • If one of your input transactions spends funds from an already used address (meaning it is subject to key re-use), we recommend that you use the bundle miner tool to craft a bundle with the most applicable security given the already exposed parts of the given address' private key.
  • Do not use too many input transactions as this will increase the overall Proof-of-Work time needed for a single bundle. Rather, split your input addresses over multiple migration bundles.
  • Your code must include logic to await for the migration bundle's confirmation. If you find that your migration bundle is not confirming, consider re-attaching it (re-attaching is not the same as re-signing the bundle).
  • If you submit a migration bundle for broadcasting via the broadcastTransactions API command and you are using the updated legacy node software, then it will additionally check up on the submission whether your submitted bundle really adheres to the rules outlined above as an additional safeguard.

For further information about the migration bundles, see the RFC-0035 page.

As an example, this bundle is a valid migration bundle. It spends 1 Mi

  • to: TRANSFERTBIXPEWWYZZWBWPWJCB9XYMC9AGYH9X9AYAYADVXTYGYB9G9J9PEF9O9KYZXS9D9MANWTZOD9B9HMRQFWZ where the destination address encodes the target Bech32 address iota1qqhmslysuwfedz2mqtr4ux73pr7uhjmd4tpazqs8pf7qdax44muqgw0fz25 respectively the hex Ed25519 address 2fb87c90e39396895b02c75e1bd108fdcbcb6daac3d102070a7c06f4d5aef804 on which the these funds will be made available in the new network.
Migration address​

As mentioned above, a migration bundle must have as its single destination/output address a migration address. A migration address is, in essence, an EdDSA address (to which you hold the keys on the new network) encoded in a legacy tryte address. You can create such an address in the following way:

  • Compute the BLAKE2b-256 hash H of your Ed25519 address A (this address is the one you control in the new network; note that an Ed25519 address is the Blake2b-256 hash of your Ed25519 public key).
  • Append the first 4 bytes of H to A, resulting in 36 bytes.
  • Convert A to trytes using the b1t6 encoding (as described in RFC-15). This results in Atri consisting of 72 trytes.
  • Prepend the 8-tryte prefix TRANSFER to Atri.
  • Finally, pad Atri with the single tryte 9 to get a legacy 81-tryte address.


  • Ed25519 address (32-byte): 6f9e8510b88b0ea4fbc684df90ba310540370a0403067b22cef4971fec3e8bb8

Since nobody holds keys to such migration addresses, funds are effectively burned and can no longer be used in the legacy network.

Migration Code Example with Node.js​

Click to see more detailed documentation for the following example.

* This example creates a new database and account,
* and migrate funds from the legacy network to the chrysalis network


// Minimum balance that is required for a migration bundle, because of the dust protection in the new network
// This value should not be too high, because then the PoW could take too long to get it confirmed

async function run() {
try {
const { AccountManager, SignerType, addEventListener } = require('@iota/wallet')

// We store all bundle hashes here and check later if the bundles got confirmed
let migrationBundleHashes = [];
// Log migration events
const callback = function (err, data) {
// After a successful broadcast of this bundle, the library will automatically reattach bundle to
// speed up the confirmation process. An event with type "TransactionConfirmed" (with corresponding bundle hash)
// will be emitted as soon as the bundle is confirmed.
if (data.event.type === 'TransactionConfirmed') {
console.log("MigrationProgress:", data)
migrationBundleHashes = migrationBundleHashes.filter(hash => hash !==
if (migrationBundleHashes.length == 0) {
console.log("Still unconfirmed bundles: ", migrationBundleHashes);
addEventListener("MigrationProgress", callback)

const manager = new AccountManager({
storagePath: './migration-database',
// Save this mnemonic securely. If you lose it, you potentially lose everything.
const mnemonic = manager.generateMnemonic()
console.log("Save this mnemonic securely. If you lose it, you potentially lose everything:", mnemonic);
manager.storeMnemonic(SignerType.Stronghold, mnemonic)

const account = await manager.createAccount({
// Node url for the new network
clientOptions: { node: "", localPow: true, network: "chrysalis-mainnet" },
alias: 'Migration',

console.log('Account created:', account.alias())
// Nodes for the legacy network
const nodes = ['']
const seed = process.env.MIGRATION_SEED
const migrationData = await manager.getMigrationData(
// permanode for the legacy network
permanode: '',
// this is the default and from there it will check addresses for balance until 30 in a row have 0 balance
// if not all balance got detected because a higher address index was used it needs to be increased here
initialAddressIndex: 0

let input_batches = getMigrationBundles(migrationData.inputs)
// create bundles with the inputs
for (batch of input_batches) {
try {
const bundle = await manager.createMigrationBundle(seed, => input.index), {
logFileName: 'iota-migration.log',
// if the input is a spent address we do a bundle mining process which takes 10 minutes to reduce the amount
// of the parts of the private key which get revealed
mine: batch.inputs[0].spent
} catch (e) {

// Send all bundles to the Tangle and reattach them until they are confirmed
for (bundleHash of migrationBundleHashes) {
try {
await manager.sendMigrationBundle(nodes, bundleHash)
} catch (e) { console.error(e) }

} catch (e) {


const getMigrationBundles = (inputs) => {
// Categorise spent vs unspent inputs
const { spent, unspent } = inputs.reduce((acc, input) => {
if (input.spent) {
} else {
return acc;
}, { spent: [], unspent: [] })
const unspentInputChunks = selectInputsForUnspentAddresses(unspent)
const spentInputs = spent.filter((input) => input.balance >= MINIMUM_MIGRATION_BALANCE)
return [ => ({
// Make sure for spent addresses, we only have one input per bundle
inputs: [input]
})), => ({ inputs }))
* Prepares inputs (as bundles) for unspent addresses.
* Steps:
* - Categorises inputs in two groups 1) inputs with balance >= MINIMUM_MIGRATION_BALANCE 2) inputs with balance < MINIMUM_MIGRATION_BALANCE
* - Creates chunks of category 1 input addresses such that length of each chunk should not exceed MAX_INPUTS_PER_BUNDLE
* - For category 2:
* - Sort the inputs in descending order based on balance;
* - Pick first N inputs (where N = MAX_INPUTS_PER_BUNDLE) and see if their accumulative balance >= MINIMUM_MIGRATION_BALANCE
* - If yes, then repeat the process for next N inputs. Otherwise, iterate on the remaining inputs and add it to a chunk that has space for more inputs
* - If there is no chunk with space left, then ignore these funds. NOTE THAT THESE FUNDS WILL ESSENTIALLY BE LOST!
* NOTE: If the total sum of provided inputs are less than MINIMUM_MIGRATION_BALANCE, then this method will just return and empty array as those funds cannot be migrated.
* This method gives precedence to max inputs over funds. It ensures a maximum a bundle could have is 30 inputs and their accumulative balance >= MINIMUM_MIGRATION_BALANCE
* @method selectInputsForUnspentAddresses
* @params {Input[]} inputs
* @returns {Input[][]}
const selectInputsForUnspentAddresses = (inputs) => {
const totalInputsBalance = inputs.reduce((acc, input) => acc + input.balance, 0);

// If the total sum of unspent addresses is less than MINIMUM MIGRATION BALANCE, just return an empty array as these funds cannot be migrated
if (totalInputsBalance < MINIMUM_MIGRATION_BALANCE) {
return [];

const { inputsWithEnoughBalance, inputsWithLowBalance } = inputs.reduce((acc, input) => {
if (input.balance >= MINIMUM_MIGRATION_BALANCE) {
} else {

return acc;
}, { inputsWithEnoughBalance: [], inputsWithLowBalance: [] })

let chunks = inputsWithEnoughBalance.reduce((acc, input, index) => {
const chunkIndex = Math.floor(index / MAX_INPUTS_PER_BUNDLE)

if (!acc[chunkIndex]) {
acc[chunkIndex] = [] // start a new chunk


return acc
}, [])

const fill = (_inputs) => {
_inputs.every((input) => {
const chunkIndexWithSpaceForInput = chunks.findIndex((chunk) => chunk.length < MAX_INPUTS_PER_BUNDLE);

if (chunkIndexWithSpaceForInput > -1) {
chunks =, idx) => {
if (idx === chunkIndexWithSpaceForInput) {
return [...chunk, input]

return chunk

return true;

// If there is no space, then exit
return false;

const totalBalanceOnInputsWithLowBalance = inputsWithLowBalance.reduce((acc, input) => acc + input.balance, 0)

// If all the remaining input addresses have accumulative balance less than the minimum migration balance,
// Then sort the inputs in descending order and try to pair the
if (totalBalanceOnInputsWithLowBalance < MINIMUM_MIGRATION_BALANCE) {
const sorted = inputsWithLowBalance.slice().sort((a, b) => b.balance - a.balance)

} else {
let startIndex = 0

const sorted = inputsWithLowBalance.slice().sort((a, b) => b.balance - a.balance)
const max = Math.ceil(sorted.length / MAX_INPUTS_PER_BUNDLE);

while (startIndex < max) {
const inputsSubset = sorted.slice(startIndex * MAX_INPUTS_PER_BUNDLE, (startIndex + 1) * MAX_INPUTS_PER_BUNDLE)
const balanceOnInputsSubset = inputsSubset.reduce((acc, input) => acc + input.balance, 0);

if (balanceOnInputsSubset >= MINIMUM_MIGRATION_BALANCE) {
chunks = [...chunks, inputsSubset]
} else {


return chunks;

After the migration, only the 24-word mnemonic or the stronghold file gives you access to the funds, so make sure to back them up properly. It is impossible to get access to the funds with the old seed after the migration transaction. Please read our recommendations for Backup and security.