Appearance
JavaScript/TypeScript Guide
INFO
This guide targets @iroha2/client
and @iroha/data-model
version ^1.2
.
1. Client Installation
The Iroha 2 JavaScript library consists of multiple packages:
Package | Description |
---|---|
client | Submits requests to Iroha Peer |
data-model | Provides SCALE (Simple Concatenated Aggregate Little-Endian)-codecs for the Iroha 2 Data Model |
crypto-core | Contains cryptography types |
crypto-target-node | Provides compiled crypto WASM (Web Assembly) for the Node.js environment |
crypto-target-web | Provides compiled crypto WASM for native Web (ESM) |
crypto-target-bundler | Provides compiled crypto WASM to use with bundlers such as Webpack |
All of these are published under the @iroha2
scope into Iroha Nexus Registry. In the future, they will be published in the main NPM Registry. To install these packages, you first need to set up a registry:
# FILE: .npmrc
@iroha2:registry=https://nexus.iroha.tech/repository/npm-group/
Then you can install these packages as any other NPM package:
npm i @iroha2/client
yarn add @iroha2/data-model
pnpm add @iroha2/crypto-target-web
The set of packages that you need to install depends on your intention. Maybe you only need to play with the Data Model to perform (de-)serialisation, in which case the data-model
package is enough. If you only need to check on a peer in terms of its status or health, you just need the client library, because this API doesn't require any interactions with crypto or Data Model.
For the purposes of this tutorial, it's better to install everything. However, in general, the packages are maximally decoupled, so you can minimise the footprint.
Moving on, if you are planning to use the Transaction or Query API, you'll also need to inject an appropriate crypto
instance into the client at runtime. This has to be adjusted depending on your particular environment. For example, for Node.js users, such an injection may look like the following:
import { crypto } from '@iroha2/crypto-target-node'
import { setCrypto } from '@iroha2/client'
setCrypto(crypto)
INFO
Please refer to the related @iroha2/crypto-target-*
package documentation because it may require some specific configuration. For example, the web
target requires to call an asynchronous init()
function before using crypto
.
2. Client Configuration
The JavaScript Client is fairly low-level in a sense that it doesn't expose any convenience features like a TransactionBuilder
or a ConfigBuilder
.
INFO
The work on implementing those is underway, and these features will very likely be available in the second round of this tutorial's release.
Thus, on the plus side, configuration of the client is simple. On the downside, you have to prepare a lot manually.
You may need to use transactions or queries, so before we initialize the client, let's set up this part. Let's assume that you have stringified public & private keys (more on that later). Thus, a key-pair generation could look like this:
import { crypto } from '@iroha2/crypto-target-node'
import { KeyPair } from '@iroha2/crypto-core'
// the package for hex-bytes transform
import { hexToBytes } from 'hada'
function generateKeyPair(params: {
publicKeyMultihash: string
privateKey: {
digestFunction: string
payload: string
}
}): KeyPair {
const multihashBytes = Uint8Array.from(
hexToBytes(params.publicKeyMultihash),
)
const multihash = crypto.createMultihashFromBytes(multihashBytes)
const publicKey = crypto.createPublicKeyFromMultihash(multihash)
const privateKey = crypto.createPrivateKeyFromJsKey(params.privateKey)
const keyPair = crypto.createKeyPairFromKeys(publicKey, privateKey)
// don't forget to "free" created structures
for (const x of [publicKey, privateKey, multihash]) {
x.free()
}
return keyPair
}
const kp = generateKeyPair({
publicKeyMultihash:
'ed0120e555d194e8822da35ac541ce9eec8b45058f4d294d9426ef97ba92698766f7d3',
privateKey: {
digestFunction: 'ed25519',
payload:
'de757bcb79f4c63e8fa0795edc26f86dfdba189b846e903d0b732bb644607720e555d194e8822da35ac541ce9eec8b45058f4d294d9426ef97ba92698766f7d3',
},
})
A basic client setup requires a Torii configuration and an account ID. This allows you to perform basic operations like health or status checks. As described above, to use transactions or queries you'll need to have a keyPair
parameter as a part of the Client
instance definition:
import { Client } from '@iroha2/client'
const client = new Client({
torii: {
// Both URLs are optional in case you only need one of them,
// e.g. only the telemetry endpoints
apiURL: 'http://127.0.0.1:8080',
telemetryURL: 'http://127.0.0.1:8081',
},
accountId: AccountId({
// Account name
name: 'alice',
// The domain where this account is registered
domain_id: DomainId({
name: 'wonderland',
}),
}),
// A key pair, needed for transactions and queries
keyPair: kp,
})
3. Registering a Domain
Here we see how similar the JavaScript code is to the Rust counterpart. It should be emphasised that the JavaScript library is a thin wrapper: It doesn't provide any special builder structures, meaning you have to work with bare-bones compiled Data Model structures and define all internal fields explicitly.
Doubly so, since JavaScript employs many implicit conversions, we highly recommend that you employ TypeScript. This makes many errors far easier to debug, but, unfortunately, results in more boilerplates.
Let's register a new domain named looking_glass
using our current account, alice@wondeland.
First, we need to import necessary models and a pre-configured client instance:
import { Client } from '@iroha2/client'
import {
DomainId,
EvaluatesToRegistrableBox,
Executable,
Expression,
IdentifiableBox,
Instruction,
MapNameValue,
Metadata,
NewDomain,
OptionIpfsPath,
QueryBox,
RegisterBox,
Value,
VecInstruction,
} from '@iroha2/data-model'
// --snip--
declare const client: Client
To register a new domain, we need to submit a transaction with a single instruction: to register a new domain. Let's wrap it all in an async function:
async function registerDomain(domainName: string) {
const registerBox = RegisterBox({
object: EvaluatesToRegistrableBox({
expression: Expression(
'Raw',
Value(
'Identifiable',
IdentifiableBox(
'NewDomain',
NewDomain({
id: DomainId({
name: domainName,
}),
metadata: Metadata({ map: MapNameValue(new Map()) }),
logo: OptionIpfsPath('None'),
}),
),
),
),
}),
})
await client.submit(
Executable(
'Instructions',
VecInstruction([Instruction('Register', registerBox)]),
),
)
}
Which we use to register the domain like so:
await registerDomain('looking_glass')
We can also use Query API to ensure that the new domain is created. Let's create another function that wraps that functionality:
async function ensureDomainExistence(domainName: string) {
// Query all domains
const result = await client.request(QueryBox('FindAllDomains', null))
// Display the request status
console.log('%o', result)
// Obtain the domain
const domain = result
.as('Ok')
.result.as('Vec')
.map((x) => x.as('Identifiable').as('Domain'))
.find((x) => x.id.name === domainName)
// Throw an error if the domain is unavailable
if (!domain) throw new Error('Not found')
}
Now you can ensure that domain is created by calling:
await ensureDomainExistence('looking_glass')
4. Registering an Account
Registering an account is a bit more involved than registering a domain. With a domain, the only concern is the domain name. However, with an account, there are a few more things to worry about.
First of all, we need to create an AccountId
. Note that we can only register an account to an existing domain. The best UX design practices dictate that you should check if the requested domain exists now, and if it doesn't, suggest a fix to the user. After that, we can create a new account named white_rabbit.
Imports we need:
import {
AccountId,
DomainId,
EvaluatesToRegistrableBox,
Expression,
IdentifiableBox,
Instruction,
MapNameValue,
Metadata,
NewAccount,
PublicKey,
RegisterBox,
Value,
VecPublicKey,
} from '@iroha2/data-model'
The AccountId
structure:
const accountId = AccountId({
name: 'white_rabbit',
domain_id: DomainId({
name: 'looking_glass',
}),
})
Second, you should provide the account with a public key. It is tempting to generate both it and the private key at this time, but it isn't the brightest idea. Remember that the white_rabbit trusts you, alice@wonderland, to create an account for them in the domain looking_glass, but doesn't want you to have access to that account after creation.
If you gave white_rabbit a key that you generated yourself, how would they know if you don't have a copy of their private key? Instead, the best way is to ask white_rabbit to generate a new key-pair, and give you the public half of it.
const pubKey = PublicKey({
payload: new Uint8Array([
/* put bytes here */
]),
digest_function: 'some_digest',
})
Only then do we build an instruction from it:
const registerAccountInstruction = Instruction(
'Register',
RegisterBox({
object: EvaluatesToRegistrableBox({
expression: Expression(
'Raw',
Value(
'Identifiable',
IdentifiableBox(
'NewAccount',
NewAccount({
id: accountId,
signatories: VecPublicKey([pubKey]),
metadata: Metadata({ map: MapNameValue(new Map()) }),
}),
),
),
),
}),
}),
)
Which is then wrapped in a transaction and submitted to the peer the same way as in the previous section when we registered a domain.
5. Registering and minting assets
Now we must talk a little about assets. Iroha has been built with few underlying assumptions about what the assets need to be.
The assets can be fungible (every £1 is exactly the same as every other £1), or non-fungible (a £1 bill signed by the Queen of Hearts is not the same as a £1 bill signed by the King of Spades), mintable (you can make more of them) and non-mintable (you can only specify their initial quantity in the genesis block).
Additionally, the assets have different underlying value types. Specifically, we have AssetValueType.Quantity
, which is effectively an unsigned 32-bit integer, a BigQuantity
, which is an unsigned 128-bit integer, and Fixed
, which is a positive (though signed) 64-bit fixed-precision number with nine significant digits after the decimal point. All three types can be registered as either mintable or non-mintable.
In JS, you can create a new asset with the following construction:
import {
NewAssetDefinition,
AssetDefinitionId,
AssetValueType,
DomainId,
EvaluatesToRegistrableBox,
Expression,
IdentifiableBox,
Instruction,
MapNameValue,
Metadata,
Mintable,
RegisterBox,
Value,
} from '@iroha2/data-model'
const newTimeAsset = NewAssetDefinition({
value_type: AssetValueType('Quantity'),
id: AssetDefinitionId({
name: 'time',
domain_id: DomainId({ name: 'looking_glass' }),
}),
metadata: Metadata({ map: MapNameValue(new Map()) }),
mintable: Mintable('Not'), // If only we could mint more time.
})
const register = Instruction(
'Register',
RegisterBox({
object: EvaluatesToRegistrableBox({
expression: Expression(
'Raw',
Value(
'Identifiable',
IdentifiableBox('NewAssetDefinition', newTimeAsset),
),
),
}),
}),
)
Pay attention to the fact that we have defined the asset as Mintable('Not')
. What this means is that we cannot create more of time
. The late bunny will always be late, because even the super-user of the blockchain cannot mint more of time
than already exists in the genesis block.
This means that no matter how hard the white_rabbit tries, the time that he has is the time that was given to him at genesis. And since we haven't defined any time in the domain looking_glass at genesis and defined time in a non-mintable fashion afterwards, the white_rabbit is doomed to always be late.
If we had set mintable: Mintable('Infinitely')
on our time asset, we could mint it:
import {
AssetDefinitionId,
DomainId,
EvaluatesToIdBox,
EvaluatesToValue,
Expression,
IdBox,
AssetId,
AccountId,
Instruction,
MintBox,
Value,
} from '@iroha2/data-model'
const mint = Instruction(
'Mint',
MintBox({
object: EvaluatesToValue({
expression: Expression('Raw', Value('U32', 42)),
}),
destination_id: EvaluatesToIdBox({
expression: Expression(
'Raw',
Value(
'Id',
IdBox(
'AssetId',
AssetId({
account_id: AccountId({
name: 'alice',
domain_id: DomainId({
name: 'wonderland',
}),
}),
definition_id: AssetDefinitionId({
name: 'time',
domain_id: DomainId({ name: 'looking_glass' }),
}),
}),
),
),
),
}),
}),
)
Again it should be emphasised that an Iroha 2 network is strongly typed. You need to take special care to make sure that only unsigned integers are passed to the Value.variantsUnwrapped.U32
factory method. Fixed precision values also need to be taken into consideration. Any attempt to add to or subtract from a negative Fixed-precision value will result in an error.
6. Visualizing outputs
Finally, we should talk about visualising data. The Rust API is currently the most complete in terms of available queries and instructions. After all, this is the language in which Iroha 2 was built.
Let's build a small Vue 3 application that uses each API we've discovered in this guide!
Our app will consist of 3 main views:
- Status checker that periodically requests peer status (e.g. current blocks height) and shows it;
- Domain creator, which is a form to create a new domain with specified name;
- Listener with a toggle to setup listening for events.
Our client config is the following (config.json
file in the project):
{
"torii": {
"apiURL": "http://127.0.0.1:8080",
"telemetryURL": "http://127.0.0.1:8081"
},
"account": {
"name": "alice",
"domain_id": {
"name": "wonderland"
}
},
"publicKey": "ed01207233bfc89dcbd68c19fde6ce6158225298ec1131b6a130d1aeb454c1ab5183c0",
"privateKey": {
"digestFunction": "ed25519",
"payload": "9ac47abf59b356e0bd7dcbbbb4dec080e302156a48ca907e47cb6aea1d32719e7233bfc89dcbd68c19fde6ce6158225298ec1131b6a130d1aeb454c1ab5183c0"
}
}
To use these, firstly, we need to initialize our client and crypto.
// FILE: crypto.ts
import { init, crypto } from '@iroha2/crypto-target-web'
// using top-level module await
await init()
export { crypto }
// FILE: client.ts
import { Client, setCrypto } from '@iroha2/client'
import { KeyPair } from '@iroha2/crypto-core'
import { hexToBytes } from 'hada'
import { AccountId } from '@iroha2/data-model'
// importing already initialized crypto
import { crypto } from './crypto'
// a config with stringified keys
import client_config from './config'
setCrypto(crypto)
export const client = new Client({
torii: {
// these ports are specified in the peer's own config
apiURL: `http://localhost:8080`,
telemetryURL: `http://localhost:8081`,
},
// Account name and the domain where it's registered
accountId: client_config.account as AccountId,
// A key pair, required for the account authentication
keyPair: generateKeyPair({
publicKeyMultihash: client_config.publicKey,
privateKey: client_config.privateKey,
}),
})
// an util function
function generateKeyPair(params: {
publicKeyMultihash: string
privateKey: {
digestFunction: string
payload: string
}
}): KeyPair {
const multihashBytes = Uint8Array.from(
hexToBytes(params.publicKeyMultihash),
)
const multihash = crypto.createMultihashFromBytes(multihashBytes)
const publicKey = crypto.createPublicKeyFromMultihash(multihash)
const privateKey = crypto.createPrivateKeyFromJsKey(params.privateKey)
const keyPair = crypto.createKeyPairFromKeys(publicKey, privateKey)
for (const x of [publicKey, privateKey, multihash]) {
x.free()
}
return keyPair
}
Now we are ready to use the client. Let's start from the StatusChecker
component:
<script setup lang="ts">
import { useIntervalFn, useAsyncState } from '@vueuse/core'
import { client } from '../client'
const { state: status, execute: updateStatus } = useAsyncState(
() => client.getStatus(),
null,
{
resetOnExecute: false,
},
)
useIntervalFn(() => updateStatus(), 1000)
</script>
<template>
<div>
<h3>Status</h3>
<ul v-if="status">
<li>Blocks: {{ status.blocks }}</li>
<li>Uptime (sec): {{ status.uptime.secs }}</li>
</ul>
</div>
</template>
Now let's build the CreateDomain
component:
<script setup lang="ts">
import {
DomainId,
EvaluatesToRegistrableBox,
Executable,
Expression,
IdentifiableBox,
Instruction,
MapNameValue,
Metadata,
NewDomain,
OptionIpfsPath,
RegisterBox,
Value,
VecInstruction,
} from '@iroha2/data-model'
import { ref } from 'vue'
import { client } from '../client'
const domainName = ref('')
const isPending = ref(false)
async function register() {
try {
isPending.value = true
await client.submit(
Executable(
'Instructions',
VecInstruction([
Instruction(
'Register',
RegisterBox({
object: EvaluatesToRegistrableBox({
expression: Expression(
'Raw',
Value(
'Identifiable',
IdentifiableBox(
'NewDomain',
NewDomain({
id: DomainId({
name: domainName.value,
}),
metadata: Metadata({
map: MapNameValue(new Map()),
}),
logo: OptionIpfsPath('None'),
}),
),
),
),
}),
}),
),
]),
),
)
} finally {
isPending.value = false
}
}
</script>
<template>
<div>
<h3>Create Domain</h3>
<p>
<label for="domain">New domain name:</label>
<input id="domain" v-model="domainName" />
</p>
<p>
<button @click="register">
Register domain{{ isPending ? '...' : '' }}
</button>
</p>
</div>
</template>
And finally, let's build the Listener
component that will use Events API to set up a live connection with a peer:
<script setup lang="ts">
import { SetupEventsReturn } from '@iroha2/client'
import {
FilterBox,
OptionHash,
OptionPipelineEntityKind,
OptionPipelineStatusKind,
PipelineEntityKind,
PipelineEventFilter,
PipelineStatusKind,
} from '@iroha2/data-model'
import {
computed,
onBeforeUnmount,
shallowReactive,
shallowRef,
} from 'vue'
import { bytesToHex } from 'hada'
import { client } from '../client'
interface EventData {
hash: string
status: string
}
const events = shallowReactive<EventData[]>([])
const currentListener = shallowRef<null | SetupEventsReturn>(null)
const isListening = computed(() => !!currentListener.value)
async function startListening() {
currentListener.value = await client.listenForEvents({
filter: FilterBox(
'Pipeline',
PipelineEventFilter({
entity_kind: OptionPipelineEntityKind(
'Some',
PipelineEntityKind('Transaction'),
),
status_kind: OptionPipelineStatusKind(
'Some',
PipelineStatusKind('Committed'),
),
hash: OptionHash('None'),
}),
),
})
currentListener.value.ee.on('event', (event) => {
const { hash, status } = event.as('Pipeline')
events.push({
hash: bytesToHex([...hash]),
status: status.match({
Validating: () => 'validating',
Committed: () => 'committed',
Rejected: (_reason) => 'rejected with some reason',
}),
})
})
}
async function stopListening() {
await currentListener.value?.stop()
currentListener.value = null
}
onBeforeUnmount(stopListening)
</script>
<template>
<div>
<h3>Listening</h3>
<p>
<button @click="isListening ? stopListening() : startListening()">
{{ isListening ? 'Stop' : 'Listen' }}
</button>
</p>
<p>Events:</p>
<ul>
<li v-for="{ hash, status } in events" :key="hash">
Transaction <code>{{ hash }}</code> status:
{{ status }}
</li>
</ul>
</div>
</template>
That's it! Finally, we just need to wrap it up with the App.vue
component and the app
entrypoint:
<script setup lang="ts">
import CreateDomain from './components/CreateDomain.vue'
import Listener from './components/Listener.vue'
import StatusChecker from './components/StatusChecker.vue'
</script>
<template>
<StatusChecker />
<hr />
<CreateDomain />
<hr />
<Listener />
</template>
<style lang="scss">
#app {
padding: 16px;
font-family: sans-serif;
}
</style>
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
Here is a small demo with the usage of this component: