Skip to main content

examples

Examples

warning

It is not recommended to store passwords/seeds on host's environment variables or in the source code in a production setup! Please make sure you follow our backup and security recommendations for production use!

Connecting to node(s)#

All features of iota.rs library are accessible via an instance of Client class that provides high-level abstraction to all interactions over IOTA network (Tangle). This class has to be instantiated before starting any interactions with the library, or more precisely with IOTA nodes that power IOTA network.

In nodejs binding, the Client instance is instantiated and optionally configured via chaining calls of ClientBuilder helper class.

The library is designed to automatically choose a starting IOTA node based on the network type one would like to participate in: testnet or mainnet. So very simplistic example how to connect to IOTA testnet is the following one:


function run() {    const { ClientBuilder } = require('@iota/client');
    // client will connect to testnet by default    const client = new ClientBuilder()        .localPow(true)        .build();
    client.getInfo().then(console.log).catch(console.error);}
run()

Output example of getInfo() function of the ClientBuilder instance:

{   "nodeinfo":{      "name":"HORNET",      "version":"0.6.0-alpha",      "isHealthy":true,      "networkId":"migration",      "bech32HRP":"atoi",      "minPoWScore":100,      "messagesPerSecond":4.2,      "referencedMessagesPerSecond":4.1,      "referencedRate":97.61904761904762,      "latestMilestoneTimestamp":1618139001,      "latestMilestoneIndex":7092,      "confirmedMilestoneIndex":7092,      "pruningIndex":0,      "features":[         "PoW"      ]   },   "url":"https://api.lb-0.h.chrysalis-devnet.iota.cafe"}

The most important properties:

  • isHealthy: indicates whether the given node is in sync with the network and so it is safe to use it. Even if a node is up and running it may not be fully prepared to process your API calls properly. The node should be "synced", meaning should be aware of all TXs in the Tangle. It is better to avoid not fully synced nodes
  • bech32HRP: indicates whether the given node is a part of testnet (atoi) or mainnet (iota). See more info regarding IOTA address format

Please note, when using node load balancers then mentioned health check may be quite useless since follow-up API calls may be served by different node behind the load balancer that may have not been actually checked. One should be aware of this fact and trust the given load balancer participates only with nodes that are in healthy state. iota.rs library additionally supports a management of internal node pool and so load-balancer-like behavior can be mimicked using this feature locally.

Needless to say, the ClientBuilder helper class provides several chaining calls via which the process can be closely managed.

The most common ones:

  • .network(str): can be testnet or mainnet. It instructs the library whether to automatically select testnet nodes or mainnet nodes
  • .node(url): specify address of actual running IOTA node that should be used to communicate with (in format https://node:port), for ex: https://api.lb-0.h.chrysalis-devnet.iota.cafe:443
  • .nodePoolUrls(urls): library also supports a management of pool of nodes. You can provide a list of nodes and library manages access to them automatically (selecting them based on their sync status)
  • .localPow(bool): .localPow (True) (by default) means a Proof-of-work is done locally and not remotely
  • .disableNodeSync(): when called, it means library also uses nodes that are not in sync with network. This parameter is usually useful if one would like to interact with local test node that is not fully synced. This parameter should not be used in production

If .nodePoolUrls(urls) is provided then the library periodically checks in some interval (call .nodeSyncInterval(interval)) whether node is in sync or not.

Example of use of additional initialization chaining calls, such as leveraging a custom node:


function run() {    const { ClientBuilder } = require('@iota/client');
    // client will connect to testnet by default    const client = new ClientBuilder()        .node('https://api.lb-0.h.chrysalis-devnet.iota.cafe:443')    // custom node        .localPow(true)                                         // pow is done locally        .disableNodeSync()                                      // even non-synced node is fine - do not use in production        .build();
    client.getInfo().then(console.log).catch(console.error);}
run()

Generating seed and addresses#

Since the IOTA network is a permission-less type of network, anybody is able to use it and interact with it. No central authority is required at any stage. So anybody is able to generate their own seed and then deterministically generate respective private keys/addresses.

info

Please note, it is highly recommended to NOT use online seed generators at all. The seed is the only key to the given addresses. Anyone who owns the seed also owns all funds related to respective IOTA addresses (all of them).

warning

We strongly recommend to use the official wallet.rs library together with stronghold.rs enclave for value-based transfers. This combination incorporates the best security practices while dealing with seeds, related addresses and UTXO. See more information on Chrysalis docs.

IOTA uses Ed25519 signature scheme and address is usually represented by Bech32 (checksummed base32) format string of 64 characters.

A root of Ed25519 signature scheme is basically a 32-byte (256-bit) uniformly randomly generated seed based on which all private keys and corresponding addresses are generated. In the examples below, the seed is represented by a string of 64 characters using [0-9a-f] alphabet (32 bytes encoded in hexadecimal).

A seed can be, for example, generated using the SHA256 algorithm on some random input generated by a cryptographically secure pseudo-random generator, such as crypto.randomBytes():

function run() {    const crypto = require('crypto');    const seed = crypto.createHash('sha256').update(crypto.randomBytes(256)).digest('hex');    console.log(seed);
    const { ClientBuilder } = require('@iota/client');    const client = new ClientBuilder().build();
    const mnemonic = client.generateMnemonic();    console.log(mnemonic);
    const hexEncodedSeed = client.mnemonicToHexSeed(mnemonic);    console.log(hexEncodedSeed);}
run()

Output example:

39bccf7b88a8017e6a96e6f31e34f138829c574dc6061523e84c5f2e53f5ca36pass phrase weapon yellow diary scissors gift drive strategy antique scheme make surround aerobic mystery coral hope lock walnut become exclude only glove syrupeff5c97c96ddab55d6fe78f914508750152eaab1b9692236bc79268895ecfd168e91eedd2489ed6c51fc44156b9a2e6c967e4edcfb649ff33d41581be4627347
info

In modern wallet implementations, such as our wallet.rs library and firefly wallet, the seed is usually generated from a seed mnemonic (seed phrase), using BIP39 standard, to be better memorized/stored by humans. It is based on a randomly generated list of english words and later used to generate the seed. Either way, the seed is a root for all generated private keys and addresses.

Address/key space#

Before an actual address generation process, let's quickly focus on the BIP32 standard that describes an approach to Hierarchical Deterministic Wallets. The standard was improved by BIP44 lately.

These standards define a tree structure as a base for address and key space generation which is represented by a derivation path:

m / purpose / coin_type / account / change / address_index
  • m: a master node (seed)
  • purpose: a constant which is {44}
  • coin_type: a constant set for each crypto currency. IOTA = 4218, for instance.
  • account: account index. Zero-based increasing int. This level splits the address/key space into independent branches (ex. user identities) which each has own set of addresses/keys
  • change: change index which is {0, 1}, also known as wallet chain.
    There are two independent chains of addresses/keys. 0 is reserved for public addresses (for coin receival) and 1 is reserved for internal (also known as change) addresses to which transaction change is returned. IOTA is totally fine with address reuse, and so it is, technically speaking, totally valid to return transaction change to the same originating address. So it is up to developers whether to leverage it or not. iota.rs library and its sibling wallet.rs help with either scenario
  • address_index: address index. Zero-based increasing int that indicates an address index

As outlined, there is a quite large address/key space that is secured by a single unique seed.

And there are few additional interesting notes:

  • Each level defines a completely different subtree (subspace) of addresses/keys and those are never mixed up
  • The hierarchy is ready to "absorb" addresses/keys for many different coins at the same time (coin_type), and all those coins are secured by the same seed.
    (So basically any BIP32/44-compliant wallet is potentially able to manage any BIP32/44-compliant coin(s))
  • There may be also other purposes in the future, however, let's consider a single purpose for now. The constant 44 stands for BIP44
  • The standard was agreed upon different crypto communities, although not all derivation path components are always in active use. For example, account is not always actively leveraged across crypto space (if this is the case then account=0 is usually used)
  • Using different accounts may be useful to split addresses/keys into some independent spaces and it is up to developers to implement.
    Please note, it may have a negative impact on a performance while account discovery phase. So if you are planning on using many multiple accounts then you may be interested in our stateful library wallet.rs that incorporates all business logic needed to efficiently manage independent accounts. Also our exchange guide provides some useful tips on how different accounts may be leveraged

address_generation

So in case of IOTA, the derivation path of address/key space is [seed]/44/4218/{int}/{0,1}/{int}. The levels purpose and coin_type are given, the rest levels are up to developers to integrate.

Generating address(es)#

IOTA addresses are generated via AddressGetter helper class by calling the Client.getAddresses() function and respective chaining calls that return a list of tuples with generated addresses. Considering the previous chapter about individual address/key spaces, it becomes quite clear what all used input function arguments are for.

Please note: for the examples outlined below, an example seed b3d7092195c36d47133ff786d4b0a1ef2ee6a0052f6e87b6dc337935c70c531e was used via environment variable called IOTA_SEED_SECRET. This seed serves for training purposes only.

The whole process is deterministic which means the output is the same as long as the seed is the same:

async function run() {  const { ClientBuilder } = require('@iota/client');
  // Get the seed from environment variable  const IOTA_SEED_SECRET = process.env.IOTA_SEED_SECRET;
  // client will connect to testnet by default  const client = new ClientBuilder().build();
  const addresses = await client.getAddresses(IOTA_SEED_SECRET)    .accountIndex(0)    .range(0, 5)    .get();
  console.log(addresses);}
run()

Output example:

[  'atoi1qz6dr6dtl0856tf0pczz7gesrf7j8a4vr00q58ld2zx7ttlv3p96snpym9z',  'atoi1qpp7sz28a0ghvd6knwnljr7j2s04qquduuc5vlz94fwf94zznj2yv5ew2c4',  'atoi1qzje6zhg5vu456eg3z84ekcfn3laxqyczche5eeqhcdh3w9yr5sqvr4z4td',  'atoi1qqwhxjmcvmatpedeedapgx0vwyupfwx9k5n4w0lnc5l6vmz78aavwhs55v0',  'atoi1qzg63t9880jtfysvpq7rrynz0rqt3kd2fw8r4934ezraz9dpwvzxkw2dtmh']

IOTA addresses are represented by a checksumed base-32 string (Bech32) and you can see a detailed explanation on Chrysalis docs. Just a recap:

  • If an address starts with atoi then it means it is related to testnet. iota stands for mainnet
  • Number 1 at 5th position is just a separator
  • The last 6 characters are reserved for a checksum

Addresses can be also represented in a hex format and luckily iota.rs provides some convenience functions to convert addresses respectively: Client.bech32ToHex(bech32) and Client.hexToBech32(hex, bech32_hrp (optional)).

To quickly validate any IOTA address, there is a convenience function Client.isAddressValid() that returns a bool value. Needless to say, performing a sanity check of an address before its use is an advisable practice.

Checking a balance#

In the Chrysalis testnet, there is a faucet service that provides test tokens to any testnet address: https://faucet.testnet.chrysalis2.com/

There are three common api calls that can be leveraged:

  • Client.getAddressBalance(str): it expects a single address in Bech32 format and returns dict with a balance for the address
  • Client.getAddressBalances([]): a convenience function that expects list of addresses in Bech32 format and returns list of dict with balances for all given addresses
  • Client.getBalance(seed): a convenience helper BalanceGetter class that combines Client.getAddresses() and Client.getAddressBalance() api calls. It returns a combined balance for the provided seed and optional chaining calls .accountIndex(index), .initialAddressIndex(index) and .gapLimit(amount)
async function run() {    const { ClientBuilder } = require('@iota/client');
    // Get the seed from environment variable    const IOTA_SEED_SECRET = process.env.IOTA_SEED_SECRET;
    // client will connect to testnet by default    const client = new ClientBuilder().build();
    // Get the balance of a single known address    console.log(        await client.getAddressBalance("atoi1qp9427varyc05py79ajku89xarfgkj74tpel5egr9y7xu3wpfc4lkpx0l86")    );
    // Get the balance of addresses from an account    const balance = await client.getBalance(IOTA_SEED_SECRET)        .accountIndex(0)        .initialAddressIndex(0)        .get();
    console.log("Account balance: " + balance);}
run()

Example of output:

{   "address":"atoi1qp9427varyc05py79ajku89xarfgkj74tpel5egr9y7xu3wpfc4lkpx0l86",   "balance":10000000,   "dustAllowed":false}Account balance: 0

Client.getBalance(seed) performs a several tasks under the hood. It starts generating addresses for the provided seed and .accountIndex from .initialAddressIndex(index), and checks for a balance of each of the generated addresses. Since it does not know how many addresses are used in fact, there is a condition set by the .gapLimit(amount) argument to know when to stop searching. If the .gapLimit amount of addresses in a row have no balance, the function returns results and searching does not continue.

Messages, payload and transactions#

Before we continue, let's introduce some additional terms that describe a unit that is actually broadcasted in the IOTA network. IOTA is based on a concept of messages and payloads.

Message is a data structure that is actually being broadcasted in the IOTA network and represents a node (vertex) in the Tangle graph. It can refer to up to 8 previous messages and once a message is attached to the Tangle and approved by a milestone, the Tangle structure ensures the content of the message is unaltered. Every message is referenced by message_id which is based on a hash algorithm of binary content of the message. Message is an atomic unit that is confirmed by the network as a whole.

info

IOTA is no longer based on ternary. IOTA 1.5 (Chrysalis) uses binary to encode and broadcast all underlying data entities

Message is broadcasted using a binary format, is arbitrary in size (up to 32 kB), and it can hold variable sets of information so called payloads. The number of payloads a single message can encapsulate is not given (even a message without any payload at all is completely valid).

Payload represents a layer of concern. Some payloads may change a state of the ledger (ex. transactions) and some may provide extra features to some specific applications and business use cases (ex. indexed data).

There are already implemented core payloads, such as SignedTransaction, MilestonePayload and IndexationPayload but the message and payload definition is generic enough to incorporate any future payload(s) the community agrees upon.

Needless to say, the IOTA network ensures the outer structure of a message itself is valid and definitely aligned with a network consensus protocol, however the inner structure is very flexible, future-proof, and offers unmatched network extensibility.

messages_in_tangle

The current IOTA network incorporates the following core payloads:

  • SignedTransaction: payload that describes UTXO transactions that are the cornerstone of value-based transfers in the IOTA network. Via this payload, message can be also cryptographically signed
  • MilestonePayload: payload that is emitted by Coordinator
  • IndexationPayload: payload that enables addition of an index to the encapsulating message, as well as some arbitrary data. The given index can be later used to search the message(s)

Unspent Transaction Output (UTXO)#

IOTA uses the unspent transaction output model, so called UTXO. It is based on an idea to track unspent amount of tokens via a data structure called output.

Simplified analogy:

  • There are 100 tokens recorded in the ledger as Output A and this output belongs to Alice. So initial state of ledger: Output A = 100 tokens
  • Alice sends 20 tokens to Paul, 30 tokens to Linda and keeps 50 tokens at her disposal
  • Her 100 tokens are recorded as Output A and so she has to divide (spent) tokens and create three new outputs:
    Output B with 20 tokens that goes to Paul, Output C with 30 tokens that goes to Linda and finally Output D with the remaining 50 tokens that she keep for herself
  • Original Output A was completely spent and can't be used any more. It has been spent and so it becomes irrelevant to the ledger state
  • New state of ledger: Output B = 20 tokens, Output C = 30 tokens and Output D = 50 tokens
  • Total supply remains the same. Just the number of outputs differ and some outputs were replaced by other outputs in the process

utxo

The key takeaway of the outlined process is the fact that each unique output can be spent only once. Once the given output is spent, it can't be used any more and is irrelevant in regards to the ledger state.

So even if Alice still wants to keep the remaining tokens at her fingertips, those tokens have to be moved to completely new output that can be, for instance, still tied to Alice's same iota address as before.

Every output also stores information about an IOTA address to which it is coupled with. So addresses and tokens are indirectly coupled via outputs. So basically the sum of outputs and their amounts under the given address is a balance of the given address, ie. the number of tokens the given address can spend. And the sum of all unspent outputs and their amounts is equal to the total supply.

Before the chapter is wrapped up, one thing was left unexplained: "how are outputs being sent and broadcasted to the network?" Outputs are being sent encapsulated in a message as part of the SignedTransaction payload.

Outputs#

There are three functions to get UTXO outputs (related to the given address):

  • Client.getAddressOutputs(str): it expects an address in Bech32 format and returns a str[] of output_ids
  • Client.getOutput(str): it expects an output_id and returns the UTXO output metadata associated with it
  • Client.findOutputs(output_ids (optional), addresses (optional)): it is a bit more general and it searches for UTXO outputs associated with the given output_ids and/or addresses
async function run() {    const { ClientBuilder } = require('@iota/client');
    // client will connect to testnet by default    const client = new ClientBuilder().build();
    const outputs = await client.getAddressOutputs('atoi1qp9427varyc05py79ajku89xarfgkj74tpel5egr9y7xu3wpfc4lkpx0l86');    console.log(outputs);}
run()

Output example:

[  '0f2d5d2651f8061a9f5417d0658009f32b2e3f77f9706b0be3b4b3f466171f360000',  '7614ba900a90b130707766a660a454942ac7cc4adea3fb9ad0cdca90114417c20000',  '768c20c15a290e02a43b83263a98501b9d7eb0b57da40a9247289c672de63ea60000']

Then the function Client.getOutput(str) can be used to get metadata about the given output_id:

async function run() {    const { ClientBuilder } = require('@iota/client');
    // client will connect to testnet by default    const client = new ClientBuilder().build();
    const output = await client.getOutput('a22cba0667c922cbb1f8bdcaf970b2a881ccd6e88e2fcce50374de2aac7c37720000');    console.log(output);}
run()

Output example:

{  "messageId": "f303bc90a5ed3ef15af5fc6aa81a739978c59458a71e68ce8e380f1f534da1e6",  "transactionId": "0f2d5d2651f8061a9f5417d0658009f32b2e3f77f9706b0be3b4b3f466171f36",  "outputIndex": 0,  "isSpent": false,  "address": "atoi1qzt0nhsf38nh6rs4p6zs5knqp6psgha9wsv74uajqgjmwc75ugupx3y7x0r",  "amount": 1000000}

A function Client.findOutputs() is a convenient shortcut combining both mentioned methods in a single call:

async function run() {    const { ClientBuilder } = require('@iota/client');
    // client will connect to testnet by default    const client = new ClientBuilder().build();
    const outputs = await client.findOutputs(outputIds = [], addresses = ["atoi1qp9427varyc05py79ajku89xarfgkj74tpel5egr9y7xu3wpfc4lkpx0l86"]);    console.log(outputs);}
run()
  • it supports two arguments, a list of output_ids or a list of addresses

Output example:

[  {    "messageId": "f303bc90a5ed3ef15af5fc6aa81a739978c59458a71e68ce8e380f1f534da1e6",    "transactionId": "0f2d5d2651f8061a9f5417d0658009f32b2e3f77f9706b0be3b4b3f466171f36",    "outputIndex": 0,    "isSpent": false,    "address": "atoi1qzt0nhsf38nh6rs4p6zs5knqp6psgha9wsv74uajqgjmwc75ugupx3y7x0r",    "amount": 1000000  },  {    "messageId": "825266a79c0ffb6001ed263eb150357863b7d0052627c5766e8ef5acd6fed533",    "transactionId": "768c20c15a290e02a43b83263a98501b9d7eb0b57da40a9247289c672de63ea6",    "outputIndex": 0,    "isSpent": false,    "address": "atoi1qzt0nhsf38nh6rs4p6zs5knqp6psgha9wsv74uajqgjmwc75ugupx3y7x0r",    "amount": 1000000  }]
  • message_id: refers to the encapsulating message in which the transaction was sent
  • transaction_id, output_index: refer to the given output within the SignedTransaction payload. There may be several different outputs involved in a single transaction so just transaction_id is not enough
  • output: this section provides details about the iota address to which the given unspent transaction output is coupled with
  • amount: states an amount of tokens related to the output
  • is_spent: of course, is a very important one indicating whether the given output is a part of the actual ledger state or not. As mentioned above, if an output was already spent, it is not part of the ledger state any more and was replaced by some other output(s) in the process

So this is quite an interesting part, notice that the output_id that was used in a function call to get output details is the same as a combination of transaction_id and output index.

This way a transaction is tightly coupled with outputs since the SignedTransaction payload is a main vehicle how outputs are being created and spent, and altogether everything is encapsulated in a message.

Messages#

As mentioned above, the message is encapsulating data structure that is being actually broadcasted across the network. It is an atomic unit that is accepted/rejected as a whole.

There is a function Client.postMessage(message) that accepts a message instance and sends it over a network. Alternatively, there is also a convenience MessageSender helper class with respective chaining calls that prepares a message instance and broadcasts it over the network.

The simplest message that can be broadcasted is a message without any particular payload:

async function run() {    const { ClientBuilder } = require('@iota/client');
    // client will connect to testnet by default    const client = new ClientBuilder().build();
    const messageId = await client.message().submit();    console.log(messageId);}
run()

Output example:

{  message: {    networkId: '14379272398717627559',    parentMessageIds: [      '03ddc83fad172a322fb00fb4e449436e9d1117ff390879100647c650a30c2d52',      '252798210fa9816f6fd40f1b19095da9f2dc88ae06fc4c0523a928a29d0d782e',      'a8e4f4cd49227068424ead8da187a48fdaa7ce8ffc4b9ac0ee2d5d3f2fcd7e70',      'dbbc8044bc624b3378e1dda83ab95f9be468b06a6a9806c76a70353182028cf9'    ],    payload: null,    nonce: '9223372036854784215'  },  messageId: '10dbee9cf3c58507725861b34ac711058dc13f709be1a6d21f1dc0af17b06379'}
  • message_id is a unique id that refers to the given message in the network

Once a message is broadcasted, there is the MessageFinder helper class instantiated via Client.getMessage() function that provides helper functions related to the given message, such as Client.getMessage().data(str) and Client.getMessage().metadata(str):

async function run() {    const { ClientBuilder } = require('@iota/client');
    // client will connect to testnet by default    const client = new ClientBuilder().build();
    // get message data by message id (get a random message id with getTips)    const tips = await client.getTips();    const message_data = await client.getMessage().data(tips[0]);    const message_metadata = await client.getMessage().metadata(tips[0]);    console.log(message_metadata);    console.log(message_data);
    // get indexation data by index    const message_ids = await client.getMessage().index("IOTA.RS BINDING - NODE.JS")    for (message_id of message_ids) {        const message_wrapper = await client.getMessage().data(message_id)        console.log(Buffer.from(message_wrapper.message.payload.data, 'hex').toString('utf8'));    }}
run()

Output example:

Message meta data:{   "messageId":"e52b631bc7500366b90c6e11eb7fd6abaa7527f9bb5b4b512b0b9112bb9e7be8",   "parentMessageIds": [      "26d72339ed262c1ec29d6c91de6be26d067b3327191f5e47606df53cc40e334e",      "6289ea0aecf3830e5e8d9925959bb6e804e324bb6db23c5701f7a538d12831f6",      "fdbf2d02603235fdff99f0ceb57705ead95041d62de386387f2922e5d9f6c502",      "ffa26139ca7f9d4849e118ff369fb3a387c8fefd8d15232b8353d4acf334324c"   ],   "isSolid":true,   "shouldPromote":false,   "shouldReattach":false}
Message data:{   "message": {      "networkId":"14379272398717627559",      "parentMessageIds": [         "27782707e4cbf84ca26b3db881bbf39b6429f9ee736a0cbe5a1c177d7a52b05d",         "61cdf92c64a3304bbbabaf9fbfb0ea7ef9624e1eedea68efbe08595ccdf853e1",         "a222d13e3ee51b56b0b0e38140a5f7f813b6d9e29b752d7e1e2424099455080d",         "ab6bca20091b58dcbb0906438a7e47bfb11621c4a37b8d118b565f7f138a40d6"      ],      "payload": {         "type":2,         "index":"484f524e4554205370616d6d6572",         "data":"42696e61727920697320746865206675747572652e0a436f756e743a2031333936393530390a54696d657374616d703a20323032312d30352d33315431353a33363a30392b30323a30300a54697073656c656374696f6e3a203337c2b573"         },      "nonce":"246736"   },   "messageId":"30d87fa9917602e5685638e37802bde11b260bd2379f6c850704d7babd365b44"}
  • Client.getMessage().metadata() provides information on how the given message fits to network structures such as ledger_inclusion_state, etc.
  • Client.getMessage().data() provides all data that relates to the given message and its payload(s)

IndexationPayload#

IndexationPayload is a payload type that can be used to attach an arbitrary data and key index to a message. At least index should be provided in order to send the given payload. Data part (as bytes[]) is optional one:

async function run() {    const { ClientBuilder } = require('@iota/client');
    // client will connect to testnet by default    const client = new ClientBuilder().build();
    const message = await client.message()        .index('IOTA.RS BINDING - NODE.JS')        .data('some utf based data')        .submit();
    console.log(message);}
run()

Output example:

{   "message": {      "networkId":"14379272398717627559",      "parentMessageIds": [         "1a383abbe5f6a6b0899d718975c3119643aa784a68d04075f4e986fd7a0c0e4b",         "6098f889e31911833df7b7839e8b222d701ab496f7dfa1a719087edf4fa7ae52",         "a98b47db4e8254eccc738c968bd35b08a5491e56d6c1a18af298c42bbd8c3a46",         "da6796c0842c08de832c7948fffedc0d5adce372e50a108f26a128dba6096d31"      ],      "payload": {         "type":2,         "index":"494f54412e52532042494e44494e47202d204e4f44452e4a53",         "data":"736f6d65207574662062617365642064617461"      },      "nonce":"13835058055282176519"   },   "messageId":"10f59c101cec669b0a0ba163bc777184c7f63455f5e771d42f910a1ba2ad20ff"}
  • Feel free to check the given message using its message_id via Tangle explorer
  • There are three payloads prepared (transaction, milestone and indexation) however only indexation payload is leveraged this time
  • data contains an arbitrary data encoded in bytes
  • Please note there is no IOTA address involved while sending data messages. Such messages are referenced using message_id or key index
  • IOTA addresses are part of the UTXO data structure that is sent using the SignedTransaction payload explained below

SignedTransaction#

SignedTransaction is a payload type that is used to transfer value-based messages as UTXO (Unspent Transaction Output).

As mentioned above, this core payload changes the ledger state as old outputs are being spent (replaced) and new outputs are being created:

async function run(){    const { ClientBuilder } = require('@iota/client');
    // client will connect to testnet by default    const client = new ClientBuilder().build();
    const message_data = await client.getMessage().data("92f427d68c7008a81fde290b9cb99071373d9893d65718bfc22928273877e041");    console.log(message_data);}
run()

Example of a message with a SignedTransaction payload:

{    "message": {        "networkId": "14379272398717627559",        "parentMessageIds": [            "a59a5d11da0944c88b58f9f9c095c11ee4b8b7fd9da47bd25412d39f815bb804",            "c3d42c42eccd25bc3374a0552e3a4b21180facece14f31c36e5ac580e5496ccc",            "dae4a36cef9a3fd03caff5ddbc5c90bc5523477f4e4937837202bfe4bd5b98aa",            "fe188a4f57ecd6a135b05b31913d86617550d9397476ab5bb7445138f782ec34"        ],        "payload": {            "type": 0,            "essence": {                "type": 0,                "inputs": [                    {                        "type": 0,                        "transactionId": "b2b9723c9119f4fb49084472e72821e842ba4779df02e1038f03dd8b25d96730",                        "transactionOutputIndex": 1                    }                ],                "outputs": [                    {                        "type": 0,                        "address": {                            "type": 0,                            "address": "43e80947ebd17637569ba7f90fd2541f50038de731467c45aa5c92d4429c9446"                        },                        "amount": 1000                    },                    {                        "type": 0,                        "address": {                            "type": 0,                            "address": "b4d1e9abfbcf4d2d2f0e042f23301a7d23f6ac1bde0a1fed508de5afec884ba8"                        },                        "amount": 8995995                    }                ],                "payload": null            },            "unlockBlocks": [                {                    "type": 0,                    "signature": {                        "type": 0,                        "publicKey": "27177dd41cc479ed379b8ad2535d66fa58480c119a8a15a7a296f055401ab958",                        "signature": "8403dc1fb949365e960f14cdc19b6b3abb6b0a6bce83f1082a33e3857a30ddd2be1098074b6c261f442db8e59eb640002d24d9a577262fd8152c6fee2d076c0b"                    }                }            ]        },        "nonce": "156106"    },    "messageId": "92f427d68c7008a81fde290b9cb99071373d9893d65718bfc22928273877e041"}

Each transaction includes the following set of information:

  • inputs: list of valid outputs that should be used to fund the given message. Those outputs will be spent and once the message is confirmed, those outputs are not valid anymore. Outputs are uniquely referenced via transaction_id and inner index. At least one output has to be given with enough balance to source all outputs of the given message
  • outputs: list of IOTA address(es) and related amount(s) that the input outputs should be split among. Based on this information, new UTXO entities (outputs) are being created
  • unlock_blocks: it includes a transaction signature(s) (currently based on Ed25519 scheme) that proofs token ownership based on a valid seed. Needless to say, only the valid seed owner is able to correctly sign the given transaction and proof the ownership of tokens under the given output(s). Each input output has to have a corresponding unblockBlocks entry in case more outputs are used to fund the operation either using the given signature or as a reference to the existing signature
  • payload: each SignedTransaction(payload type 0) can include additional payload(s) such as IndexationPayload (payload type 1), etc. Meaning, any value-based messages can also contain arbitrary data and its key index. It is also an example how individual payloads can be encapsulated on different levels of concern

Sending value-based messages is also a very straightforward process via the MessageSender helper class.

As a minimum, it needs a valid seed, output addresses, and amount. The method finds valid output(s) that can be used to fund the given amount(s) and the unspent amount is sent to the same address:

async function run() {    const {        ClientBuilder    } = require('@iota/client');
    // Get the seed from environment variable    const IOTA_SEED_SECRET = process.env.IOTA_SEED_SECRET;
    // client will connect to testnet by default    const client = new ClientBuilder().build();
    const message = await client.message()        .seed(IOTA_SEED_SECRET)        .output('atoi1qqydc70mpjdvl8l2wyseaseqwzhmedzzxrn4l9g2c8wdcsmhldz0ulwjxpz', 1000000)        .submit();
    console.log(message);}
run()
info

We recommend to use the official wallet.rs library together with stronghold.rs enclave for value-based transfers. This combination incorporates the best security practices while dealing with seeds, related addresses and UTXO. See more information on Chrysalis docs.

Dust protection#

Please also note, there is a dust protection mechanism implemented in the network protocol to avoid malicious actors to spam network in order to decrease node performance while keeping track of unspent amount (UTXO):

info

"... microtransaction below 1Mi of IOTA tokens [can be sent] to another address if there is already at least 1Mi on that address" That's why we sent 1Mi in the given example to comply with the protection."

Listening to MQTT#

IOTA node(s) provides a Message Queuing Telemetry Transport (MQTT) layer, if enabled, which is a lightweight publish-subscribe network protocol that provides information about events that is being triggered by the IOTA network.

iota.rs client library supports asynchronous event listeners that can be listened to, and continuously receive MQTT events based on a topic, which can be:

  • milestones/latest
  • milestones/confirmed
  • messages
  • messages/referenced
  • messages/indexation/{index}
  • messages/{messageId}/metadata
  • transactions/{transactionId}/included-message
  • outputs/{outputId}
  • addresses/{address}/outputs
  • addresses/ed25519/{address}/outputs

The listener is reachable via an instance of a Client.TopicSubscriber object that is returned from the Client.subscriber() function.

It offers several chaining calls:

  • .topic(str) / .topics(str[]): a topic or list of topics that should trigger a provided callback
  • .subscribe(cb): it subscribes the listener to a callback function that is being triggered every time the given topic(s) is noticed
  • .unsubscribe(cb): it unsubscribes the listener from the given topics. Once unsubscribed, then the given callback function is executed in a form (err, message) => {}
async function run() {    const {        ClientBuilder    } = require('@iota/client');
    // client connects to a node that has MQTT enabled    const client = new ClientBuilder()        .node('https://api.thin-hornet-1.h.chrysalis-devnet.iota.cafe')        .build();
    client.subscriber().topics(['milestones/confirmed', 'messages']).subscribe((err, data) => {        console.log(data);        // To get the message id from messages `client.getMessageId(data.payload)` can be used    })
    await new Promise(resolve => setTimeout(resolve, 1500));    // unsubscribe from 'messages' topic, will continue to receive events for 'milestones/confirmed'    client.subscriber().topics(['messages']).unsubscribe((err, data) => {        console.log(data);    })}
run()

Please note: The IOTA node needs to have the MQTT layer enabled. There is a set of test nodes available that have MQTT enabled. See testnet chapter for more information.