Examples

There are several examples to show the usage of the library.

All examples can be found in /bindings/nodejs/examples

Setup

First, setup your environment as follows:

git clone https://github.com/iotaledger/wallet.rs
cd bindings/node/examples
npm install # or `yarn`
cp .env.example .env

Add your custom password to the .env file.

1. Example: Create an Account

Run:

node 1-create-account.js

Code:

/**
 * This example creates a new database and account
 */

require('dotenv').config()

async function run() {
    const { AccountManager, SignerType } = require('@iota/wallet')
    const manager = new AccountManager({
        storagePath: './alice-database',
    })
    manager.setStrongholdPassword(process.env.SH_PASSWORD)
    manager.storeMnemonic(SignerType.Stronghold)

    const account = await manager.createAccount({
        clientOptions: { node: "https://api.lb-0.testnet.chrysalis2.com", localPow: true },
        alias: 'Alice',
    })

    console.log('Account created:', account.alias())
      
}

run()

2. Generate Address

Run:

node 2-generate-address.js

Code:

/**
 * This example genrates a new address.
 */

require('dotenv').config()

async function run() {
	const { AccountManager } = require('@iota/wallet')
    const manager = new AccountManager({
        storagePath: './alice-database'
    })

    manager.setStrongholdPassword(process.env.SH_PASSWORD)

    const account = manager.getAccount('Alice')
    console.log('Account:', account.alias())

    // Always sync before doing anything with the account
    const synced = await account.sync()
    console.log('Syncing...')

    const { address } = account.generateAddress()
    console.log('New address:', address)

    // You can also get the latest unused address:
    // const addressObject = account.latestAddress()
    // console.log("Address:", addressObject.address)

    // Use the Chrysalis Faucet to send testnet tokens to your address:
    console.log("Fill your address with the Faucet: https://faucet.testnet.chrysalis2.com/")
}

run()

3. Example: Check Balance

Run:

node 3-check_balance

Code:

/**
 * This example creates a new database and account
 */

require('dotenv').config()

async function run() {
	const { AccountManager } = require('@iota/wallet')
    const manager = new AccountManager({
        storagePath: './alice-database'
    })

    manager.setStrongholdPassword(process.env.SH_PASSWORD)

    const account = manager.getAccount('Alice')
    
    console.log('Account:', account.alias())
    
    // Always sync before doing anything with the account
    const synced = await account.sync()
    console.log('Syncing...')

    console.log('Available balance', account.balance().available)
}

run()

4. Example: Check Balance

Now you can send the test tokens to an address!

Run

node 4-send.js

Code:

/**
 * This example sends IOTA Toens to an address.
 */

 require('dotenv').config();

async function run() {
	const { AccountManager } = require('@iota/wallet')
    const manager = new AccountManager({
        storagePath: './alice-database'
    })

    manager.setStrongholdPassword(process.env.SH_PASSWORD)

    const account = manager.getAccount('Alice')
    
    console.log('alias', account.alias())
    console.log('syncing...')
    const synced = await account.sync()
    console.log('available balance', account.balance().available)
    
    //TODO: Replace with the address of your choice!
	const addr = 'atoi1qykf7rrdjzhgynfkw6z7360avhaaywf5a4vtyvvk6a06gcv5y7sksu7n5cs'
	const amount = 10000000

	const node_response = await synced.send(
		addr,
		amount
    ) 

    console.log(`Check your message on https://explorer.iota.org/chrysalis/message/${node_response.id}`)
}

run()

5. Backup

Run

node 5-backup.js

Code:

/**
 * This example backups your data in a secure file. 
 * You can move this file to another app or device and restore it.
 */

require('dotenv').config();

async function run() {

    const { AccountManager } = require('@iota/wallet')
    const manager = new AccountManager({
        storagePath: './alice-database'
    })

    manager.setStrongholdPassword(process.env.SH_PASSWORD)

    let backup_path = await manager.backup("./backup", process.env.SH_PASSWORD)
    
    console.log('Backup path:', backup_path)
}

run()

6. Restore

Run

node 6-restore.js

Code:

/**
 * This example restores a secured backup file. 
 */

require('dotenv').config();

async function run() {

    const { AccountManager } = require('@iota/wallet')
    const manager = new AccountManager({
        storagePath: './alice-database'
    })

    // Add the path to the file from example 5-backup.js
    // for example: ./backup/2021-02-12T01-23-11-iota-wallet-backup-wallet.stronghold
    let backup_path = "input your backup file"

    await manager.importAccounts(backup_path, process.env.SH_PASSWORD)
    const account = manager.getAccount('Alice')
    console.log('Account:', account.alias())
}

run()

7. Events

Run

node 7-events.js

Code:

/**
 * This example shows some events.
 */

require('dotenv').config()

async function run() {
    const { AccountManager, addEventListener } = require('@iota/wallet')
    const manager = new AccountManager({
        storagePath: './alice-database'
    })

    manager.setStrongholdPassword(process.env.SH_PASSWORD)

    const account = manager.getAccount('Alice')
    console.log('Account:', account.alias())

    // Always sync before doing anything with the account
    const synced = await account.sync()
    console.log('Syncing...')
    // let address = account.generateAddress()

    // get latest address
    let addressObject = account.latestAddress()

    console.log("Address:", addressObject.address)

    // Use the Chrysalis Faucet to send testnet tokens to your address:
    console.log("Fill your address with the Faucet: https://faucet.testnet.chrysalis2.com/")


    const callback = function (err, data) {
        console.log("data:", data)
    }

    addEventListener("BalanceChange", callback)

    // Possible Event Types:
    //
    // ErrorThrown
    // BalanceChange
    // NewTransaction
    // ConfirmationStateChange
    // Reattachment
    // Broadcast
}

run()

8. Migration

Run

node 8-migration.js

Code:

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

require('dotenv').config()

const ADDRESS_SECURITY_LEVEL = 2
// Minimum balance that is required for a migration bundle, because of the dust protection in the new network
const MINIMUM_MIGRATION_BALANCE = 1000000
// This value shouldn't be too high, because then the PoW could take to long to get it confirmed
const 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 !== data.event.data.bundleHash)
        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: "https://chrysalis-nodes.iota.cafe", localPow: true, network: "chrysalis-mainnet" },
      alias: 'Migration',
    })

    console.log('Account created:', account.alias())
    // Nodes for the legacy network
    const nodes = ['https://nodes.iota.org']
    const seed = process.env.MIGRATION_SEED
    const migrationData = await manager.getMigrationData(
      nodes,
      seed,
      {
        // permanode for the legacy network
        permanode: 'https://chronicle.iota.org/api',
        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, batch.inputs.map(input => 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) }
    }

  } catch (e) {
    console.error(e);
  }
}

run()

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 [
    ...spentInputs.map((input) => ({
      // Make sure for spent addresses, we only have one input per bundle    
      inputs: [input]
    })),
    ...unspentInputChunks.map((inputs) => ({ 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
    }

    acc[chunkIndex].push(input)

    return acc
  }, [])

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

      if (chunkIndexWithSpaceForInput > -1) {
        chunks = chunks.map((chunk, 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)

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