Skip to main content
Deno 2 is finally here 🎉️
Learn more

Capi

Capi is a WIP TypeScript toolkit for crafting interactions with Substrate-based chains.

Capi consists of FRAME-oriented utilities and (soon) a high-level functional effect system which facilitate multistep, multichain interactions without compromising on performance or safety.

⚠ This Is a Work in Progress

️Please share feedback or even join us in Capi’s development; issues and PRs are very welcome!

In Good Shape

  • RPC call and subscribe utils
  • Metadata types and SCALE codecs
  • Metadata-based codec derivation
  • Storage key encoding and (when transparent) decoding
  • Storage value decoding
  • Creating and decoding extrinsics
  • RPC Client hooks / error handing

TODO

Setup

If you’re using Deno, import via the denoland/x specifier.

import * as C from "https://deno.land/x/capi/mod.ts";

Note: you may want to pin the version in your import specifier (https://deno.land/x/capi@x.x.x/mod.ts).

If you’re using Node, install Capi from NPM.

npm install capi

Then import as follows.

import * as C from "capi";

Configs

Introduction

Before interacting with a given chain, we must have a means of finding nodes of that chain (a config).

A Polkadot-specific config is accessible from capi/known.

// Deno
import { polkadot } from "https://deno.land/x/capi/known/mod.ts";
// Node
import { polkadot } from "capi/known";

The static type of any config can describe accessible RPC server methods and FRAME metadata. This enables a narrowly-typed experience to flow through all usage of Capi.

const polkadot = C.chain(polkadot);

Better yet, let’s import polkadot directly from capi/known.

import { polkadot } from "capi/known";

Testing

During development, connecting to a live network can slow down the feedback loop. It also may be infeasible, in the case that you are without an internet connection. In these situations, you can utilize Capi’s test utilities.

+ const node = await C.test.node();
+
- const chain = C.chain(myConfig);
+ const chain = C.test.chain(node);

//

+ node.close()

Here, we’ve spun up a tiny, temporary chain. You can even access accounts (and their corresponding signers).

const { alice } = chain.address;

For convenience, we’ll be utilizing the test chain and addresses.

Read a Balance

const alicePublicKey = chain.address.alice.asPublicKeyBytes();

const value = await chain
  .pallet("System")
  .entry("Account", alicePublicKey)
  .read();

Read at Specific Block Hash

const block = chain.block(BLOCK_HASH);

// ...

const value = await chain
  .pallet("System")
  .entry("Account", alicePublicKey)
  .read(block);

Note About Typings

Signatures

The signature value is a union of Read<unknown> and all possible error types.

assertTypeEquals<
  typeof value,
  C.Read<unknown> | C.WsRpcError | C.StorageEntryDneError | C.StorageValueDecodeError
>();

Narrow Error Handling

We can utilize an instanceof check to narrow the result before accessing the read value.

if (result instanceof Error) {
  // Handle narrow error types here
} else {
  // Handle `C.Read<unknown>` here
}

Transfer Some Dot

import * as C from "../mod.ts";

// ...

const { alice, bob } = chain.address;

const result = chain
  .pallet("Balances")
  .extrinsic("transfer")
  .call({
    dest: alice.asPublicKeyBytes(),
    value: 12345n,
  })
  .signed(bob, bob.sign)
  .send();

for await (const event of result) {
  console.log({ event });
}

Note: it is up to the developer to supply sign with a signing function, which will vary depending on your environment, wallet, misc.

Contributing

In the future, Gitpod and dev containers will simplify spinning up a Capi development environments. The Dockerfile, Gitpod configuration and Dev Containers / Codespaces configuration are in need some finessing.

Make sure you have the following installed on your machine (and please submit issues if errors crop up).

System Requirements

Running an Example

deno task run <path-to-example>

Building Wasm & Its Corresponding JS Bindings

deno task build:wasm

Utilizing the Package in a NodeJS Project

Build the NPM package and link it locally.

deno task dnt && cd target/npm && npm link

Then link to Capi from your NodeJS project.

npm link capi

Code Structure

You may have noticed that the Capi repository looks somewhat different from a traditional TypeScript repository. This is because Capi is developed Deno-first. Deno is a TypeScript runtime and toolkit written in Rust. Unlike NodeJS, Deno emphasizes web standards and exposes a performant and type-safe standard library. Deno-first TypeScript can be easily packaged for consumption in NodeJS, Browsers, CloudFlare Workers and other environments. Some things to note:

No src nor Distinct package/*

We no longer need to think about the separation of code for the sake of packaging. We can think about separation of code in terms of what best suits our development needs.

For example, exports of util/types.ts can be imported directly into any other TypeScript file, without specifying the dependency in a package manifest. We are free to use (for example) U2I, the union to intersection utility, in out-of-band processes, the effect system or even GitHub workflow scripts. From anywhere in the repository, we can import and use any code with configuration overhead.

When it comes time to build our code for NPM distribution, DNT takes care of transforming our dependency graph into something that NodeJS and web browsers will understand.