Stardust Exchange Integration Guide
The Stardust update is a set of protocol changes that were first deployed to the Shimmer network.
See What is Stardust?
for an overview of its features or TIPs flagged Stardust
for a deeper understanding of the protocol changes.
The Stardust update allows for complex tokenization schemes that can lead to monetary losses if misused. Transaction outputs might have multiple unlocking conditions. These conditions may require the return of part or all of the output amounts, have an expiration date if not claimed on time, or they may not be unlockable for an extended period of time. Therefore, you need to review these unlock conditions carefully before accepting any deposits to avoid loss of funds.
Integration Guide
This guide explains how to integrate the IOTA SDK into your exchange, custody solution, or product.
Features of the IOTA SDK:
- 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 IOTA SDK is a stateful library which provides an API for developers to build value transaction applications. It offers abstractions to handle payments and can optionally interact with Stronghold for seed handling, seed storage, and state backup.
If you are unfamiliar with the IOTA SDK, 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
bindings:
- Set up the IOTA SDK.
- Create an account for each user.
- Generate a user address to deposit funds.
- Check the user balance.
- Listen to events.
- Enable withdrawals.
If you are looking for other languages, please read the IOTA SDK overview.
Since all IOTA SDK bindings are based on core principles provided by the IOTA SDK, the outlined approach is very similar regardless of your chosen programming language.
If you were already supporting Shimmer with wallet.rs
The IOTA SDK is a brand new project which is a combination and improvement of both iota.rs and wallet.rs. If you were already supporting Shimmer with wallet.rs, we encourage you to switch to the SDK as wallet.rs will be deprecated and not receive any updates anymore.
Most of the APIs are the same and the changes are fairly minimal, here are some of the major changes:
- The npm package changes from
@iota/wallet
to@iota/sdk
AccountManager
becomesWallet
manager.generateMnemonic()
becomesUtils.generateMnemonic()
generateAddress
becomesgenerateEd25519Addresses
sendAmount
becomessend
- The callbacks to
listen
now provide a properly typedEvent
1. Set Up the IOTA SDK
First, you should install the components needed to use the IOTA SDK and the binding of your choice; it may vary a bit from language to language. In 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/sdk dotenv
We use dotenv
to share user-defined constants, like database paths, node URLs, and wallet credentials across many examples. This is for convenience only and shouldn't be done in production.
You can create a .env
by running the following command:
touch .env
The following examples will need these variables to exist, here are some default values you can use but also change:
WALLET_DB_PATH="./wallet-database"
NODE_URL="https://api.testnet.shimmer.network"
STRONGHOLD_SNAPSHOT_PATH="./wallet.stronghold"
STRONGHOLD_PASSWORD="*K6%79#yeFn7AMQhMQ"
EXPLORER_URL="https://explorer.shimmer.network/testnet"
1 Generate a mnemonic
loading...
After you have updated the .env
file, you can initialize the Wallet
instance with a secret manager(Stronghold
by default) and client options.
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 Stronghold
is that is impossible to extract a mnemonic from it ever again, that is, recover it from the .stronghold
file. That's why you should always back up your 24-word mnemonic and have it stored in safe place. You deal with accounts using the Wallet
instance exclusively, which abstracts away all internal complexities and makes transacting in a network very easy and convenient.
Keep the stronghold
password and the stronghold
database on separate devices. See the backup and security guide for more information.
2 Create an Account for Each User
You can import the IOTA SDK and create a wallet using the following example:
loading...
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 its alias
.
You get an instance of any created account using Wallet.getAccount(accountId|alias)
or get all accounts with Wallet.getAccounts()
.
Common methods of an Account
instance include:
account.addresses()
- returns a list of addresses related to the account.account.generateEd25519Addresses()
- generates one or more new address(es) 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
The wallet
module of the IOTA SDK 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.
loading...
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.addresses()
method:
const addresses = account.addresses();
console.log('Need a refill? Send it to this address:', addresses[0]);
You can use the Faucet to request 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.generateEd25519Addresses()
are indicated asinternal=false
(public). - Internal addresses (
internal=true
) are calledchange
addresses and are used to send the excess funds to them.
4. Check the Account Balance
Outputs may have multiple UnlockConditions, which may require returning some or all of the transferred amount. The outputs could also expire if not claimed in time, or may not be unlockable for a predefined period.
To get outputs with only the AddressUnlockCondition
, you should synchronize with the option syncOnlyMostBasicOutputs: true
.
If you are synchronizing outputs with other unlock conditions, you should check the unlock conditions carefully before crediting users any balance.
You can find an example illustrating how to check if an output has only the address unlock condition, where the address belongs to the account in the Check Unlock Conditions how-to guide.
You can get the available account balance across all addresses of the given account using the following example:
loading...
5. Listen to Events
Outputs may have multiple UnlockConditions, which may require returning some or all of the transferred amount. The outputs could also expire if not claimed in time, or may not be unlockable for a predefined period.
To get outputs with only the AddressUnlockCondition
, you should synchronize with the option syncOnlyMostBasicOutputs: true
.
If you are synchronizing outputs with other unlock conditions, you should check the unlock conditions carefully before crediting users any balance.
You can find an example illustrating how to check if an output has only the address unlock condition, where the address belongs to the account in the Check Unlock Conditions how-to guide.
The IOTA SDK 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:
loading...
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.
loading...
The full function signature is account.send(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?: TaggedDataPayload;
customInputs?: string[];
}
The account.send()
function returns a transaction
with its id. The blockId
can be used later for checking the confirmation status. You can obtain individual transactions related to the given account using the account.transactions()
function.
When sending tokens, you should consider a dust protection mechanism.