The most optimal way to interact with the Native API on The Root Network is via @polkadot/api
package in combination with our package @therootnetwork/api
. It provides web3 developers the ability to query and interact with The Root Network node using Javascript/Typescript. For installation instructions, see Install Native API.
One of the most important things to understand about the @polkadot/api
is that most interfaces are actually generated automatically when connected to a running node. This is quite a departure from other APIs in projects with static interfaces. While it may sound intimidating, this concept is actually a powerful feature present in any Substrate chain. It enables the API to be used in environments where the chain is customized.
To unpack this, we will start with the metadata and explain what it actually provides, since it is critical to understand how to interact with the API and any underlying chain.
When the API connects to a node, one of the first things it does is retrieve the metadata and decorate the API based on the metadata information. The metadata effectively provides data in the form of api.<type>.<module>.<section>
that fits into one of the following categories:
consts
- All runtime constants, e.g. api.consts.balances.existentialDeposit
. These are not functions; rather, accessing the endpoint immediately yields the result as defined.
query
- All chain state, e.g. api.query.system.account(<accountId>)
.
tx
- All extrinsics, e.g. api.tx.balances.transfer(<accountId>, <value>)
.
Additionally, the metadata also provides information on events
. These are queryable via the api.query.system.events()
interface and also appear on transactions. Both these cases are detailed later.
None of the information contained within the api.{consts, query, tx}.<module>.<method>
endpoints are hardcoded in the API. Rather, everything is fully decorated by what the metadata exposes and is, therefore, completely dynamic. This means that when you connect to different chains, the metadata and API decoration will change and the API interfaces will reflect what is available on the chain you are connected to.
Everything the API returns is a type with a consistent interface: Codec
. This means that a Vec<u32>
(an array of u32
values), as well as a Struct
(an object) or an Enum
, has the same consistent base interface. Specific types will have values based on the type - decorated and available.
As a minimum, anything returned by the API, be it a Vec<...>
, Option<...>
, Struct
or any normal type will always have the following methods - as defined on the Codec
interface:
.eq(<other value>)
- checks for equality against the other value. In all cases, it will accept "like" values, i.e. in the case of a number, you can pass a primitive (such as 1
), a hex value (such as 0x01
) or even a Unit8Array
toHex()
- returns a hex-base representation of the value, always prefixed by 0x
toHuman()
- returns Human-parsable JSON structure with values formatted as per the settings
toJSON()
- returns a JSON-like representation of the value. This is generally used when calling JSON.stringify(...)
on the value
toString()
- returns a string representation. In some cases, this performs additional encoding, i.e. for Address
, AccountId
and AccountIndex
, it will encode to the ss58 address
.toU8a()
- returns a Uint8Array
representation of the encoded value (generally exactly as passed to the node, where values are SCALE encoded)
Additionally, the following getters and utilities are available:
.isEmpty
- true
if the value is an all-empty value, i.e. 0
in for numbers, all-zero for Arrays (or anything Uint8Array
), false
is non-zero
.hash
- a Hash
(once again with all the methods above) that is a blake2-256
representation of the contained value
In addition to the api.[consts | query | tx]
detailed above, the API, upon connecting to a chain, fills in some information and makes it available directly on the API interface. These include -
api.genesisHash
- The genesisHash of the connected chain
api.runtimeMetadata
- The metadata as retrieved from the chain
api.runtimeVersion
- The chain runtime version (including specification/implementation versions and types)
The api.rpc.<section>.<method>
provides low-level interfaces to read the chain state that's not necessarily bound to the pallet storage. It's also the commonplace to add additional data query for the custom pallets, e.g. api.rpc.tokenUri
.
It's possible to subscribe to RPC calls and receive up-to-date data. For instance, subscribe to a new block height change.
Since we are dealing with a subscription, we now pass a callback into the subscribeNewHeads
function, and this will be triggered on each header as they are imported. The same pattern applies to each of the api.rpc.subscribe
functions. As the last parameter, a callback should be provided to stream the latest data as it becomes available.
In general, whenever we create a subscription, we would like to clean up after ourselves and unsubscribe. Assuming we only want to log the first 10 headers, the above example demonstrates the use of the unsubscribe()
function.
The api.query.<pallet>.<method>
interfaces are used to read the public storage in each available pallet. Behind the scenes, the API uses the metadata information provided to construct queries based on the location and parameters provided to generate state keys and then queries these via RPC.
Similar to RPC calls, sometimes it's necessary to subscribe to the chain state to handle your application logic. Fortunately, the API provides a straightforward way to do that.
Instead of the await
returning the actual one-time value, it returns a subscription unsubscribe()
function. This function can be used to stop the subscription and clean up any underlying RPC connections. The supplied callback will contain the value streamed from the node as it changes.
Transaction endpoints are exposed, as determined by the metadata, on the api.tx.<pallet>.<method>
endpoint. These allow you to submit transactions for inclusion in blocks, whether it's transfers, setting information or any other operation your chain supports.
The Root Network supports the ECDSA key-pair standard. To submit a transaction, make sure you have a valid account with a small amount of XRP
tokens to cover the transaction fee. Create a keyring
instance as follows:
Once that is done, to transfer a balance from Alice to Bob as per the example above, use the code below:
The result of this call is the transaction hash. This is a hash of the data; receiving it does not mean the transaction has been included. It only means that the hash has been accepted for propagation by the node.
Caution: Transaction hash in Substrate-based blockchain is generally unique within a block but not across the chain. Check out this article for more details.
Similar to api.rpc
and api.query
interfaces, it's possible to subscribe to the transaction state change to determine if it's been included in the block or not.
As per all previous subscriptions, the transaction subscription returns unsubscribe()
and the actual method has a subscription callback. The result
object has two parts: events
(to be covered in the next section) and the status
enum.
When the status
enum is in Finalized
state (checked via isFinalized
), the underlying value contains the block's hash where the transaction has been finalized. Finalized
will follow InBlock
, which is the block where the transaction has been included. InBlock
does not mean the block is finalized, but rather applies to the transaction state, where Finalized
means that the transaction cannot be forked off the chain.
Any transaction will emit events. As a bare minimum, this will always be either a system.ExtrinsicSuccess
or system.ExtrinsicFailed
event for the specific transaction. These provide the overall execution result for the transaction, i.e. execution has succeeded or failed.
Depending on the transaction sent, some other events may be emitted. For instance, for a balances.transfer
this could include one or more of Transfer
, NewAccount
or ReapedAccount
, as defined in the substrate balances
event defaults.
To display or act on these events, we can do the following:
Caution: Be aware that when a transaction status is isFinalized
, it means it is included, but it may still have failed - for instance, if you try to send a larger amount that you have free, the transaction is included in a block. However, from an end-user perspective, the transaction failed since the transfer did not occur. In these cases, a system.ExtrinsicFailed
event will be available in the events array.
The RPC endpoints expose weight/payment information that takes an encoded extrinsic and calculates the on-chain weight fees for it. A wrapper for this is available on the tx itself, taking exactly the same parameters as you would pass to a normal .signAndSend
operation, specifically .paymentInfo(sender, <any options>)
. To expand on our previous example: