Skip to main content

Chrysalis Migration

By the end of the Chrysalis migration, a number of 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.



Token Migration Overview#


  1. You enter your seed in Firefly.
  2. Firefly creates you a new seed and generates an EdDSA address for the new network.
  5. Your funds are successfully migrated.




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


For a detailed explaination on how the migration process works technically, see migration-mechanism

Exchange Token Migration Guide#


Note: On the 28th of April, 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 will co-exist.



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

Migrating Funds by Issuing Migration Bundles#

Note that as mentioned above, there was a special release for the legacy node software on the 28th of April, which will only further support migration bundles. In case you're 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 further confirm. A migration bundle is nothing else than a normal value bundle/transfer which has some additional restrictions. If you craft a bundle which obeys to the following rules, then it falls under what we define as a migration bundle:

  • Input transactions must not use migration addresses.

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

Things to consider:

  • You must not broadcast your own migration bundles unless you're 100% sure that they are indeed valid migration bundles.
  • If you submit a migration bundle for broadcasting via the broadcastTransactions API command and you're using the updated legacy node software (which you must on/after the 28th of April), then it will additionally check up on submission whether your submitted bundle really adheres to the rules outlined above as an additional safe guard.

For further information about the migration bundles, have a look at RFC-0035.


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:

  • Prepend the 8-tryte prefix TRANSFER to Atri.


  • Ed25519 address (32-byte): 6f9e8510b88b0ea4fbc684df90ba310540370a0403067b22cef4971fec3e8bb8


Migration Code Example with Node.js#

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 */
const ADDRESS_SECURITY_LEVEL = 2// Minimum balance that is required for a migration bundle, because of the dust protection in the new networkconst MINIMUM_MIGRATION_BALANCE = 1000000// This value shouldn't be too high, because then the PoW could take to long to get it confirmedconst MAX_INPUTS_PER_BUNDLE = 10

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) {          process.exit()        }        console.log("Still unconfirmed bundles: ", migrationBundleHashes);      }    }    addEventListener("MigrationProgress", callback)
    const manager = new AccountManager({      storagePath: './migration-database',    })    manager.setStrongholdPassword(process.env.SH_PASSWORD)    // 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(      nodes,      seed,      {        // permanode for the legacy network        permanode: '',        securityLevel: ADDRESS_SECURITY_LEVEL,        // 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      }    )    console.log(migrationData)
    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        })        migrationBundleHashes.push(bundle.bundleHash)      } catch (e) {        console.error(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) }    }
const getMigrationBundles = (inputs) => {  // Categorise spent vs unspent inputs  const { spent, unspent } = inputs.reduce((acc, input) => {    if (input.spent) {      acc.spent.push(input)    } else {      acc.unspent.push(input)    }    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's 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 can't 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) {      acc.inputsWithEnoughBalance.push(input);    } else {      acc.inputsWithLowBalance.push(input);    }
    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;    })  }
  // 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)
    fill(sorted)  } 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 {        fill(inputsSubset)      }
      startIndex++;    }  }
  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's not possible to get access to the funds with the old seed after the migration transaction. Please read our recommendations for Backup and security.