Skip to main content

Stardust Exchange Integration Guide

note

You can easily integrate wallet.rs with your exchange, custody solution, or product.

danger

Shimmer allows for complex tokenization schemes, but they can lead to monetary losses if used incorrectly. Transaction outputs may have multiple unlocking conditions which may require returning some or all of the amount, which could expire if not claimed in time, or which may not be unlockable for a very long time. You can lose money if you do not check the unlock conditions before accepting a deposit!

Integration Guide

This guide explains how to integrate the Wallet library in your exchange.

Features of the Wallet Library:

  • Secure seed management.
  • Account management with multiple accounts and multiple addresses.
  • Confirmation monitoring.
  • Deposit address monitoring.
  • Backup and restore functionality.

How Does it Work?

The Wallet Library is a stateful package with a standardized interface for developers to build applications involving value transactions. It offers abstractions to handle payments and can optionally interact with Stronghold for seed handling, seed storage, and state backup.

note

If you are not familiar with the wallet.rs library, you can find more information in the documentation.

You can use the following examples as a guide to implementing the multi-account approach using the NodeJS binding:

  1. Set up the wallet.rs library.
  2. Create an account for each user.
  3. Generate a user address to deposit funds.
  4. Check the user balance.
  5. Listen to events.
  6. Enable withdrawals.
note

If you are looking for other languages, please read the wallet.rs library overview.

Since all wallet.rs bindings are based on core principles provided by the wallet.rs library, the outlined approach is very similar regardless of the programming language you choose.

1. Set Up the Wallet.rs Library

First, you should install the components that are needed to use wallet.rs and the binding of your choice; it may vary a bit from language to language. In the case of the NodeJs binding, it is straightforward since it is distributed via the npm package manager.

You can read more about backup and security in this guide.

npm install @iota/wallet dotenv

1 Generate a mnemonic

/**
* This example generates a new random mnemonic
*/

const { AccountManager, CoinType } = require('@iota/wallet');

async function run() {
try {
const manager = new AccountManager({
storagePath: './mnemonic-generation',
clientOptions: {},
coinType: CoinType.Shimmer,
// Placeholder can't be used for address generation or signing, but we can use it since we only want to generate a mnemonic
secretManager: "placeholder",
});

console.log('Generated mnemonic:', await manager.generateMnemonic());
// Set generated mnemonic as env variable for MNEMONIC so it can be used in 1-create-account.js

// delete unnecessary db folder again
require('fs').rmSync('./mnemonic-generation', { recursive: true, force: true });

} catch (error) {
console.log('Error: ', error);
}
process.exit(0);
}

run();

You can then create a .env by running the following command:

touch .env

You can now add your SH_PASSWORD and MNEMONIC to the .env file.

SH_PASSWORD="here is your super secure password"
MNEMONIC="here is your super secure 24 word mnemonic, it's only needed here the first time"

After you have updated the .env file, you can initialize the AccountManager instance with a secret manager(Stronghold by default) and client options.

note

Manage your password with the utmost care.

By default, the Stronghold file will be called wallet.stronghold. It will store the seed (derived from the mnemonic) that serves as a cryptographic key from which all accounts and related addresses are generated.

One of the key principles behind the stronghold is that no one can get a seed out of it, so you should also back up your 24-word mnemonic in a secure place because there is no way to recover it from the .stronghold file. You deal with accounts using the AccountManager instance exclusively, where all complexities are hidden under the hood and are dealt with securely.

note

Keep the stronghold password and the stronghold database on separate devices. See the backup and security guide for more information.

2 Create an account

You can import the Wallet Library and create an account manager using the following example:

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

require('dotenv').config();
const { AccountManager, CoinType } = require('@iota/wallet');

async function run() {
try {
const accountManagerOptions = {
storagePath: './alice-database',
clientOptions: {
nodes: ['https://api.testnet.shimmer.network'],
},
// CoinType.IOTA can be used to access Shimmer staking rewards, but it's
// recommended to use the Shimmer coin type to be compatible with other wallets.
coinType: CoinType.Shimmer,
secretManager: {
Stronghold: {
snapshotPath: `./wallet.stronghold`,
password: `${process.env.SH_PASSWORD}`,
},
},
};

const manager = new AccountManager(accountManagerOptions);

// Mnemonic only needs to be set the first time
await manager.storeMnemonic(process.env.MNEMONIC);

const account = await manager.createAccount({
alias: 'Alice'
});
console.log('Account created:', account);

} catch (error) {
console.log('Error: ', error);
}
process.exit(0);
}

run();

The Alias must be unique and can be whatever fits your use case. The Alias is typically used to identify an account later on. Each account is also represented by an index which is incremented by one every time a new account is created. You can refer to any account via its index, or alias.

You get an instance of any created account using AccountManager.getAccount(accountId|alias) or get all accounts with AccountManager.getAccounts().

Common methods of account instance include:

  • account.addresses() - returns list of addresses related to the account.
  • account.generateAddress() - generate a new address for the address index incremented by 1.
  • account.balance() - returns the balance for the given account.
  • account.sync() - sync the account information with the tangle.

3. Generate a User Address to Deposit Funds

Wallet.rs is a stateful library. This means it caches all relevant information in storage to provide performance benefits while dealing with, potentially, many accounts and addresses.

/**
* This example generates an address for an account
*/

require('dotenv').config();
const { AccountManager } = require('@iota/wallet');

async function run() {
try {
const manager = new AccountManager({
storagePath: './alice-database',
});

await manager.setStrongholdPassword(`${process.env.SH_PASSWORD}`)

const account = await manager.getAccount('Alice');

const address = await account.generateAddress()

console.log('Address generated:', address);

} catch (error) {
console.log('Error: ', error);
}
process.exit(0);
}

run();

Every account can have multiple addresses. Addresses are represented by an index which is incremented by one every time a new address is created. You can access the addresses using the account.address() method:

    const addresses = account.addresses()

console.log('Need a refill? Send it to this address:', addresses[0])

You can use the Faucet to add test tokens and test your account.

There are two types of addresses, internal and public (external). This approach is known as a BIP32 Hierarchical Deterministic wallet (HD Wallet).

  • Each set of addresses is independent of each other and has an independent index id.
  • Addresses that are created by account.generateAddress() are indicated as internal=false (public).
  • Internal addresses (internal=true) are called change addresses and are used to send the excess funds to them.

4. Check the Account Balance

danger

Outputs may have multiple UnlockConditions which may require returning some or all of the amount, which could expire if not claimed in time, or which may not be unlockable for a very long time. To get only outputs with the AddressUnlockCondition alone that do not require additional ownership checks, synchronize with syncOnlyMostBasicOutputs: true. When synchronizing also other outputs, the unlock conditions must be carefully checked before crediting users any balance.

You can get the available account balance across all addresses of the given account using the following example:

/**
* This example gets the balance of an account
*/

require('dotenv').config();
const { AccountManager } = require('@iota/wallet');

async function run() {
try {
const manager = new AccountManager({
storagePath: './alice-database',
});

const account = await manager.getAccount('Alice');
const addressObject = await account.addresses();
console.log('Addresses before:', addressObject);

// syncOnlyMostBasicOutputs if not interested in outputs that are timelocked,
// have a storage deposit return or are nft/alias/foundry outputs
const synced = await account.sync({ syncOnlyMostBasicOutputs: true });
console.log('Syncing... - ', synced);

console.log('Available balance', await account.getBalance());

// Use the Faucet to send testnet tokens to your address:
console.log("Fill your address with the Faucet: https://faucet.testnet.shimmer.network/")
} catch (error) {
console.log('Error: ', error);
}
process.exit(0);
}

run();

5. Listen to Events

danger

Outputs may have multiple UnlockConditions which may require returning some or all of the amount, which could expire if not claimed in time, or which may not be unlockable for a very long time. To get only outputs with the AddressUnlockCondition alone that do not require additional ownership checks, synchronize with syncOnlyMostBasicOutputs: true. When synchronizing also other outputs, the unlock conditions must be carefully checked before crediting users any balance.

The Wallet.rs library supports several events for listening. A provided callback is triggered as soon as an event occurs (which usually happens during syncing).

You can use the following example to listen to new output events:

/**
* This example listen to the NewOutput event
*/

require('dotenv').config();
const { AccountManager } = require('@iota/wallet');

async function run() {
try {
const manager = new AccountManager({
storagePath: './alice-database',
});

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

const event = JSON.parse(data)
console.log("Event for account:", event.accountIndex)
console.log("data:", event.event)

// Exit after receiving an event
process.exit(0);
}

// provide event type to filter only for events with this type
manager.listen(['NewOutput'], callback);

const account = await manager.getAccount('Alice');

// Use the Faucet to send testnet tokens to your address:
console.log("Fill your address with the Faucet: https://faucet.testnet.shimmer.network/")
const addressObjects = await account.addresses();
console.log('Send funds to:', addressObjects[0].address);

// Sync every 5 seconds until the faucet transaction gets confirmed
for (let i = 0; i < 100; i++) {
await new Promise(resolve => setTimeout(resolve, 5000));

// Sync to detect new outputs
// syncOnlyMostBasicOutputs if not interested in outputs that are timelocked,
// have a storage deposit return or are nft/alias/foundry outputs
await account.sync({ syncOnlyMostBasicOutputs: true });
}

} catch (error) {
console.log('Error: ', error);
}
process.exit(0);
}

run();

Example output:

NewOutput: {
output: {
outputId: '0x2df0120a5e0ff2b941ec72dff3464a5b2c3ad8a0c96fe4c87243e4425b9a3fe30000',
metadata: [Object],
output: [Object],
isSpent: false,
address: [Object],
networkId: '1862946857608115868',
remainder: false,
chain: [Array]
},
transaction: null,
transactionInputs: null
}

Alternatively you can use account.outputs() to get all outputs that are stored in the account, or account.unspentOutputs(), to get only unspent outputs.

6. Enable Withdrawals

You can use the following example to send tokens to an address.

/**
* This example sends tokens to an address.
*/

require('dotenv').config();
const { AccountManager } = require('@iota/wallet');

async function run() {
try {
const manager = new AccountManager({
storagePath: './alice-database',
});

await manager.setStrongholdPassword(`${process.env.SH_PASSWORD}`)

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

const response = await account.sendAmount([
{
//TODO: Replace with the address of your choice!
address: 'rms1qrrv7flg6lz5cssvzv2lsdt8c673khad060l4quev6q09tkm9mgtupgf0h0',
amount: '1000000',
},
]);

console.log(response);

console.log(
`Check your block on https://explorer.testnet.shimmer.network/testnet/block/${response.blockId}`,
);
} catch (error) {
console.log('Error: ', error);
}
process.exit(0);
}

run();

The full function signature is account.sendAmount(outputs[, options]).

Default options are fine and successful; however, you can provide additional options, such as remainderValueStrategy, which can have the following values:

  • changeAddress: Send the remainder value to an internal address.
  • reuseAddress: Send the remainder value back to its original address.
  • customAddress: Send the remainder value back to a provided account address.
TransactionOptions {
remainderValueStrategy?: RemainderValueStrategy;
taggedDataPayload?: ITaggedDataPayload;
customInputs?: string[];
}

The account.sendAmount() function returns a transaction with it's id. The blockId can be used later for checking a confirmation status. You can obtain individual transactions related to the given account using the account.transactions() function.

Dust Protection

When sending tokens, you should consider a dust protection mechanism.