JavaScript/TypeScript Guide
INFO
This guide targets @iroha2/client
and @iroha/data-model
version ^5.0
, which targets Iroha 2 stable (2.0.0-pre-rc.13
, c4af68c4f7959b154eb5380aa93c894e2e63fe4e
).
INFO
This guide assumes you are familiar with Node.js and NPM ecosystem.
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.
INFO
You can also work with the sources in Iroha Javascript repository, where the active development is happening. Clone the repository and check out the iroha2
branch:
$ git clone https://github.com/hyperledger/iroha-javascript.git --branch iroha2
Please note that this guide does not cover the details of this workflow.
While we've taken great care to decouple the packages, so you could minimise their footprint, for the purposes of this tutorial, it's better to install everything.
The installation consists of two steps: setting up a registry and then installing the packages you need.
Set up a registry. In shell, run:
bash$ echo "@iroha2:registry=https://nexus.iroha.tech/repository/npm-group/" > .npmrc
Install Iroha 2 packages as any other NPM package. If you are following the tutorial, we recommend installing all of the following:
bash$ npm i @iroha2/client $ npm i @iroha2/data-model $ npm i @iroha2/crypto-core $ npm i @iroha2/crypto-target-node $ npm i @iroha2/crypto-target-web $ npm i @iroha2/crypto-target-bundler
INFO
Note that you can use other package managers, such as yarn or pnpm, for a faster installation. For example:
bash$ yarn add @iroha2/data-model $ pnpm add @iroha2/crypto-target-web
The set of packages that you need to install depends on what you are trying to achieve. If you only need to play with the Data Model to perform (de-)serialisation, the
data-model
package is sufficient. If you need to check on a peer in terms of its status or health, then you only need the client library.Install the following packages as well:
bash$ npm i hada $ npm i tsx -g
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 according to your particular environment.For example, Node.js users need the following:
tsimport { crypto } from '@iroha2/crypto-target-node' import { setCrypto } from '@iroha2/client' setCrypto(crypto)
INFO
Please refer to the documentation of the respective
@iroha2/crypto-target-*
package, because each case has specific configuration steps. For example, theweb
target needs to be initialised (via asynchronousinit()
) before you can use any cryptographic methods.
NOTE
When you are creating files in the following steps, you must place them in the same directory that contains node_modules
, like so:
╭───┬───────────────────┬──────╮
│ # │ name │ type │
├───┼───────────────────┼──────┤
│ 0 │ node_modules │ dir │
│ 1 │ addClient.ts │ file │
│ 2 │ example.ts │ file │
│ 3 │ package.json │ file │
│ 4 │ pnpm-lock.yaml │ file │
│ 5 │ registerDomain.ts │ file │
╰───┴───────────────────┴──────╯
We recommend using tsx
to run the scripts you've created. For example:
$ tsx example.ts
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'
const keyPair = crypto.KeyPair.fromJSON({
public_key: 'ed0120e555d194e8822da35ac541ce9eec8b45058f4d294d9426ef97ba92698766f7d3',
private_key: {
digest_function: 'ed25519',
payload:
'de757bcb79f4c63e8fa0795edc26f86dfdba189b846e903d0b732bb644607720e555d194e8822da35ac541ce9eec8b45058f4d294d9426ef97ba92698766f7d3',
},
})
When you have a key pair, you might create a Signer
using the key pair:
import { cryptoTypes } from '@iroha2/crypto-core'
import { Signer } from '@iroha2/client'
import { AccountId, DomainId } from '@iroha2/data-model'
// Key pair from previous step
declare const keyPair: cryptoTypes.KeyPair
const accountId = AccountId({
// Account name
name: 'alice',
// The domain where this account is registered
domain_id: DomainId({
name: 'wonderland',
}),
})
const signer = new Signer(accountId, keyPair)
Now we're able to make signatures with signer.sign(binary)
! However, to interact with Iroha, we need to be able to do more than just sign. We would need to send something to Iroha, like transactions or queries. Torii
will help us with that.
Torii
handles HTTP / WebSocket communications with Iroha. We will use it to communicate with Iroha endpoints. With the help of Torii
we can:
- Submit transactions with
Torii.submit()
- Send queries with
Torii.request()
- Listen for events with
Torii.listenForEvents()
- Listen for blocks stream with
Torii.listenForBlocksStream()
- and so on
Torii
is a stateless object, a compendium of methods. You can look at it as if it is a class with only static methods. Each method has its own requirements to be passed in — some of them only need an HTTP transport and Iroha Torii Telemetry URL, others — a WebSocket transport and Iroha Torii API URL. To better understand how Torii
is used, look at this example:
import { Torii } from '@iroha2/client'
import { VersionedSignedQueryRequest } from '@iroha2/data-model'
// --snip--
declare const query: VersionedSignedQueryRequest
const result = await Torii.request(
{
fetch,
apiURL: 'http://127.0.0.1:8080',
},
query,
)
In this example, we pass fetch
(the HTTP transport) and apiURL
as the first parameter, and the query itself as the second.
To work with Torii
, we need to know Iroha Torii URLs. Our Iroha Peer is configured to listen for API endpoints at http://127.0.0.1:8080
and for telemetry endpoints at http://127.0.0.1:8081
. Then, we need to provide appropriate HTTP / WebSocket adapters which Torii
will use[1]. These adapters depend on the environment in which you are going to use @iroha2/client
.
In Node.js, the full list of Torii
requirements (i.e. covering all its methods) will look like this:
import {
ToriiRequirementsForApiHttp,
ToriiRequirementsForApiWebSocket,
ToriiRequirementsForTelemetry,
} from '@iroha2/client'
import { adapter as WS } from '@iroha2/client/web-socket/node'
import nodeFetch from 'node-fetch'
// another alternative
import { fetch as undiciFetch } from 'undici'
const toriiRequirements: ToriiRequirementsForApiHttp &
ToriiRequirementsForApiWebSocket &
ToriiRequirementsForTelemetry = {
apiURL: 'http://127.0.0.1:8080',
telemetryURL: 'http://127.0.0.1:8081',
ws: WS,
// type assertion is acceptable here
// you can pass `undiciFetch` here as well
fetch: nodeFetch as typeof fetch,
}
TIP
In the example above, we use node-fetch
package which implements Fetch API in Node.js. However, you can use undici
as well.
INFO
fetch: nodeFetch as typeof fetch
type assertion is acceptable here for a reason. Torii
expects the "classic", native fetch
function, which is available natively in Browser. However, both node-fetch
and undici
don't provide fetch
that is 100% compatible with the native one. Since Torii
doesn't rely on those corner-features that are partially provided by node-fetch
and undici
, it's fine to ignore the TypeScript error here.
And here is a sample of full Torii
in-Browser requirements:
import {
ToriiRequirementsForApiHttp,
ToriiRequirementsForApiWebSocket,
ToriiRequirementsForTelemetry,
} from '@iroha2/client'
import { adapter as WS } from '@iroha2/client/web-socket/native'
const toriiRequirements: ToriiRequirementsForApiHttp &
ToriiRequirementsForApiWebSocket &
ToriiRequirementsForTelemetry = {
apiURL: 'http://127.0.0.1:8080',
telemetryURL: 'http://127.0.0.1:8081',
ws: WS,
fetch:
// passing globally available `fetch`, but binding it to `window`
// to avoid `TypeError: "'fetch' called on an
// object that does not implement interface Window."`
fetch.bind(window),
}
NOTE
We make fetch.bind(window)
to avoid TypeError: "'fetch' called on an object that does not implement interface Window."
.
Great! Now we have signer
and Torii
requirements to work with. Finally, we can create a Client
:
import { Client, Signer, ToriiRequirementsForApiHttp } from '@iroha2/client'
import { Executable } from '@iroha2/data-model'
// --snip--
declare const signer: Signer
declare const toriiRequirements: ToriiRequirementsForApiHttp
const client = new Client({ signer })
// `Client` will sign & wrap `Executable` into `VersionedSignedTransaction`
declare const exec: Executable
await client.submitExecutable(toriiRequirements, exec)
Client
provides useful utilities for transactions and queries. You can also use Torii
to communicate with the endpoints directly. Signer
is accessible with client.signer
.
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, ToriiRequirementsForApiHttp } 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
declare const toriiRequirements: ToriiRequirementsForApiHttp
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.submitExecutable(
toriiRequirements,
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.requestWithQueryBox(
toriiRequirements,
QueryBox('FindAllDomains', null),
)
// Display the request status
console.log('%o', result)
// Obtain the domain
const domain = result
.as('Ok')
.result.enum.as('Vec')
.map((x) => x.enum.as('Identifiable').enum.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
Iroha has been built with few underlying assumptions about what the assets need to be in terms of their value type and characteristics (fungible or non-fungible, mintable or non-mintable).
In JS, you can create a new asset with the following construction:
import {
AssetDefinition,
AssetDefinitionId,
AssetValueType,
DomainId,
EvaluatesToRegistrableBox,
Expression,
IdentifiableBox,
Instruction,
MapNameValue,
Metadata,
Mintable,
RegisterBox,
Value,
} from '@iroha2/data-model'
const time = AssetDefinition({
value_type: AssetValueType('Quantity'),
id: AssetDefinitionId({
name: 'time',
domain_id: DomainId({ name: 'looking_glass' }),
}),
metadata: Metadata({ map: MapNameValue(new Map()) }),
mintable: Mintable('Infinitely'), // If only we could mint more time.
})
const register = Instruction(
'Register',
RegisterBox({
object: EvaluatesToRegistrableBox({
expression: Expression(
'Raw',
Value('Identifiable', IdentifiableBox('AssetDefinition', time)),
),
}),
}),
)
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 {
AccountId,
AssetDefinitionId,
AssetId,
DomainId,
EvaluatesToIdBox,
EvaluatesToValue,
Expression,
IdBox,
Instruction,
MintBox,
NumericValue,
Value,
} from '@iroha2/data-model'
const mint = Instruction(
'Mint',
MintBox({
object: EvaluatesToValue({
expression: Expression('Raw', Value('Numeric', NumericValue('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('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. Transferring assets
After minting the assets, you can transfer them to another account. In the example below, Alice transfers to Mouse 100 units of time
asset:
import {
AccountId,
AssetDefinitionId,
AssetId,
DomainId,
EvaluatesToIdBox,
EvaluatesToValue,
Expression,
IdBox,
Instruction,
NumericValue,
TransferBox,
Value,
} from '@iroha2/data-model'
const domainId = DomainId({
name: 'wonderland',
})
const assetDefinitionId = AssetDefinitionId({
name: 'time',
domain_id: domainId,
})
const amountToTransfer = Value('Numeric', NumericValue('U32', 100))
const fromAccount = AccountId({
name: 'alice',
domain_id: domainId,
})
const toAccount = AccountId({
name: 'mouse',
domain_id: domainId,
})
const evaluatesToAssetId = (assetId: AssetId): EvaluatesToIdBox =>
EvaluatesToIdBox({
expression: Expression('Raw', Value('Id', IdBox('AssetId', assetId))),
})
const transferAssetInstruction = Instruction(
'Transfer',
TransferBox({
source_id: evaluatesToAssetId(
AssetId({
definition_id: assetDefinitionId,
account_id: fromAccount,
}),
),
destination_id: evaluatesToAssetId(
AssetId({
definition_id: assetDefinitionId,
account_id: toAccount,
}),
),
object: EvaluatesToValue({
expression: Expression('Raw', amountToTransfer),
}),
}),
)
7. Querying for Domains, Accounts and Assets
TODO
import { Client, ToriiRequirementsForApiHttp } from '@iroha2/client'
import { QueryBox } from '@iroha2/data-model'
declare const client: Client
declare const toriiRequirements: ToriiRequirementsForApiHttp
const result = await client.requestWithQueryBox(
toriiRequirements,
QueryBox('FindAllDomains', null),
)
const domains = result
.as('Ok')
.result.enum.as('Vec')
.map((x) => x.enum.as('Identifiable').enum.as('Domain'))
for (const domain of domains) {
console.log(
`Domain "${domain.id.name}" has ${domain.accounts.size} accounts` +
` and ${domain.asset_definitions.size} asset definitions`,
)
// => Domain "wonderland" has 5 accounts and 3 asset definitions
}
const result = await client.requestWithQueryBox(
toriiRequirements,
QueryBox('FindAllAccounts', null),
)
const accounts = result
.as('Ok')
.result.enum.as('Vec')
.map((x) => x.enum.as('Identifiable').enum.as('Account'))
for (const account of accounts) {
console.log(
`Account "${account.id.name}@${account.id.domain_id.name}" ` +
`has ${account.assets.size} assets`,
)
// => Account "alice@wonderland" has 3 assets
}
const result = await client.requestWithQueryBox(
toriiRequirements,
QueryBox('FindAllAssets', null),
)
const assets = result
.as('Ok')
.result.enum.as('Vec')
.map((x) => x.enum.as('Identifiable').enum.as('Asset'))
for (const asset of assets) {
console.log(
`Asset "${asset.id.definition_id.name}#${asset.id.definition_id.domain_id.name}" ` +
`at account "${asset.id.account_id.name}@${asset.id.account_id.domain_id.name}" ` +
`has type "${asset.value.enum.tag}"`,
)
// => Asset "rose#wonderland" at account "alice@wonderland" has type "Quantity"
}
8. Visualizing outputs in Web UI
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!
TIP
In this guide, we are roughly recreating the project that is a part of iroha-javascript
integration tests. If you want to see the full project, please refer to the @iroha2/client-test-web
sources.
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.
You can use this folder structure as a reference:
╭───┬──────────────────────────────╮
│ # │ name │
├───┼──────────────────────────────┤
│ 0 │ App.vue │
│ 1 │ client.ts │
│ 2 │ components/CreateDomain.vue │
│ 3 │ components/Listener.vue │
│ 4 │ components/StatusChecker.vue │
│ 5 │ config.json │
│ 6 │ crypto.ts │
│ 7 │ main.ts │
╰───┴──────────────────────────────╯
{
"torii": {
"apiURL": "http://127.0.0.1:8080",
"telemetryURL": "http://127.0.0.1:8081"
},
"account": {
"name": "alice",
"domain_id": {
"name": "wonderland"
}
},
"public_key": "ed01207233bfc89dcbd68c19fde6ce6158225298ec1131b6a130d1aeb454c1ab5183c0",
"private_key": {
"digest_function": "ed25519",
"payload": "9ac47abf59b356e0bd7dcbbbb4dec080e302156a48ca907e47cb6aea1d32719e7233bfc89dcbd68c19fde6ce6158225298ec1131b6a130d1aeb454c1ab5183c0"
}
}
import { crypto, init } from '@iroha2/crypto-target-web'
await init()
export { crypto }
import { Client, Signer } from '@iroha2/client'
import { adapter as WS } from '@iroha2/client/web-socket/native'
import { crypto } from './crypto'
import { client_config } from '../../config'
import { AccountId } from '@iroha2/data-model'
const HOST = window.location.host
export const toriiPre = {
// proxified with vite
apiURL: `http://${HOST}/torii/api`,
telemetryURL: `http://${HOST}/torii/telemetry`,
ws: WS,
fetch: fetch.bind(window),
}
const signer = new Signer(client_config.account as AccountId, crypto.KeyPair.fromJSON(client_config))
export const client = new Client({ signer })
<script setup lang="ts">
import { useIntervalFn } from '@vueuse/core'
import { useStaleState, useTask } from '@vue-kakuyaku/core'
import { toriiPre } from '../client'
import { Torii } from '@iroha2/client'
const { state, run } = useTask(() => Torii.getStatus(toriiPre), { immediate: true })
const stale = useStaleState(state)
useIntervalFn(run, 1000)
</script>
<template>
<div>
<h3>Status</h3>
<ul v-if="stale.fulfilled">
<li>Blocks: {{ stale.fulfilled.value.blocks }}</li>
<li>Uptime (sec): {{ stale.fulfilled.value.uptime.secs }}</li>
</ul>
</div>
</template>
<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, toriiPre } from '../client'
import { useTask } from '@vue-kakuyaku/core'
const domainName = ref('')
const { state, run: registerDomain } = useTask(async () => {
await client.submitExecutable(
toriiPre,
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'),
}),
),
),
),
}),
}),
),
]),
),
)
})
</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="registerDomain()">
Register domain{{ state.pending ? '...' : '' }}
</button>
</p>
</div>
</template>
<script setup lang="ts">
import { SetupEventsReturn, Torii } from '@iroha2/client'
import {
FilterBox,
OptionHash,
OptionPipelineEntityKind,
OptionPipelineStatusKind,
PipelineEntityKind,
PipelineEventFilter,
PipelineStatus,
PipelineStatusKind,
} from '@iroha2/data-model'
import { computed, onBeforeUnmount, shallowReactive, shallowRef } from 'vue'
import { toriiPre } from '../client'
function bytesToHex(bytes: number[]): string {
return bytes.map((byte) => byte.toString(16).padStart(2, '0')).join('')
}
interface EventData {
hash: string
status: string
}
const events = shallowReactive<EventData[]>([])
const currentListener = shallowRef<null | SetupEventsReturn>(null)
const isListening = computed(() => !!currentListener.value)
function displayStatus(status: PipelineStatus): string {
switch (status.enum.tag) {
case 'Validating':
return 'validating'
case 'Committed':
return 'committed'
case 'Rejected':
return 'rejected with some reason'
}
}
async function startListening() {
currentListener.value = await Torii.listenForEvents(toriiPre, {
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.enum.as('Pipeline')
events.push({
hash: bytesToHex([...hash]),
status: displayStatus(status),
})
})
}
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 class="events-list">
<li
v-for="{ hash, status } in events"
:key="hash"
>
Transaction <code>{{ hash }}</code> status:
{{ status }}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import CreateDomain from './components/CreateDomain.vue'
import EventListener from './components/EventListener.vue'
import StatusChecker from './components/StatusChecker.vue'
</script>
<template>
<StatusChecker />
<hr>
<CreateDomain />
<hr>
<EventListener />
</template>
<style lang="scss">
#app {
padding: 16px;
font-family: sans-serif;
}
</style>
import { createApp } from 'vue'
import App from './App.vue'
import { Logger } from '@iroha2/data-model'
import { crypto } from './crypto'
import { setCrypto } from '@iroha2/client'
setCrypto(crypto)
new Logger().mount()
localStorage.debug = '*'
createApp(App).mount('#app')
INFO
In client.ts
, we import the configuration file like this:
import { client_config } from '../../config'
Note that you need to import the config in this way because this is how the source code of this application works. You can interpret this line as import client_config from 'config.json'
.
Demo
Here is a small demo with the usage of this component:
9. Subscribing to Block Stream
You can use /block/stream
endpoint to send a subscription request for block streaming.
Via this endpoint, the client first provides the starting block number (i.e. height) in the subscription request. After sending the confirmation message, the server starts streaming all the blocks from the given block number up to the current block, and continues to stream blocks as they are added to the blockchain.
Here is an example of how to listen to the block stream:
import { Torii, ToriiRequirementsForApiWebSocket } from '@iroha2/client'
declare const requirements: ToriiRequirementsForApiWebSocket
const stream = await Torii.listenForBlocksStream(requirements, {
height: 0n,
})
stream.ee.on('block', (block) => {
const height = block.enum.as('V1').header.height
console.log('Got block with height', height)
})
We have to pass environment-specific
ws
andfetch
, because there is no way for Iroha Client to communicate with a peer in an environment-agnostic way. ↩︎