SDK v0.1.19

Arkiv SDK for TS - Getting Started

Last updated: January 2025

Home

1) Arkiv "Hello, World!"

Create your account

This allows you to interact with Arkiv Testnet. Your is safe to share. Never share your .

Safety: The account you generate here is for Arkiv Testnet/sandbox use only. Never use it on any Mainnet.

2) Voting Board

You’ve written your first entity to Arkiv - now let’s build something that feels more alive. Using the same test account and client, we’ll create a tiny Voting Board: a simple structure where people can open proposals, cast votes, and read results directly from the chain.

This part of the guide shows how a few small entities can already form a collaborative application - all powered by Arkiv.

You can keep experimenting right here in the CodePlayground, or set up the SDK locally to continue from your own environment.

1. Proposal

A single entity that defines what is being decided and how long the voting stays open (expiresIn).

2. Votes

Multiple small entities that reference the proposal by its entityKey and store each voter’s choice.

3. Tally

A read query that fetches all votes linked to a proposal and counts them - the simplest form of an on-chain result.

Next up: we’ll create the proposal entity - the anchor for every vote that follows.

3) Open Proposal

Create the decision “room”: a proposal entity with a bounded time window (Expires In). This is where votes will attach-still using the very same client/account you verified.

  • Goal: Write a proposal entity with an expiration window (Expires In).
  • Why it matters: Gives your vote stream a clear scope and predictable cost.
  • Success check: You get a proposal.entityKey (the proposal ID).
1const [proposal] = await client.createEntities([
2  {
3    data: enc.encode('Proposal: Switch stand-up to 9:30?'),
4    expiresIn: 200, // 200 seconds
5    stringAnnotations: [
6      new Annotation('type', 'proposal'),
7      new Annotation('status', 'open'),
8    ],
9    numericAnnotations: [new Annotation('version', 1)],
10  } as ArkivCreate,
11]);
12console.log('Proposal key:', proposal.entityKey);
13
14// Use entityKey as this proposal's identifier
15const proposalKey = proposal.entityKey;

4) Cast Votes

Attach votes to the proposal. Each vote is its own entity linked by proposalKey and attributed to a voter address. Same client, same journey-now with multiple actors.

  • Goal: Create votes with { type="vote", proposalKey, voter, choice }.
  • Why it matters: Votes are small, auditable facts you can query later.
  • Success check: Two vote keys print, both linked to your proposal.
1const voterAddr = (await (client as any).getOwnerAddress?.()) ?? 'unknown';
2
3const [vote1, vote2] = await client.createEntities([
4  {
5    data: enc.encode('vote: yes'),
6    expiresIn: 200, // 200 seconds
7    stringAnnotations: [
8      new Annotation('type', 'vote'),
9      new Annotation('proposalKey', proposalKey),
10      new Annotation('voter', voterAddr),
11      new Annotation('choice', 'yes'),
12    ],
13    numericAnnotations: [new Annotation('weight', 1)],
14  },
15  {
16    data: enc.encode('vote: no'),
17    expiresIn: 200, // 200 seconds
18    stringAnnotations: [
19      new Annotation('type', 'vote'),
20      new Annotation('proposalKey', proposalKey),
21      new Annotation('voter', voterAddr),
22      new Annotation('choice', 'no'),
23    ],
24    numericAnnotations: [new Annotation('weight', 1)],
25  },
26] as ArkivCreate[]);
27console.log('Votes cast:', vote1.entityKey, vote2.entityKey);

5) Batch Votes

Add many votes in one go-useful for demos, fixtures, or cross-proposal actions. You’re still operating with the same client and proposal context.

  • Goal: Create multiple vote entities in a single call.
  • Success check: Receipt count matches the number you pushed.
1const extras = Array.from({ length: 5 }, (_, i) => ({
2  data: enc.encode(`vote: yes #${i + 1}`),
3  expiresIn: 200,
4  stringAnnotations: [
5    new Annotation('type', 'vote'),
6    new Annotation('proposalKey', proposalKey),
7    new Annotation('voter', `${voterAddr}-bot${i}`),
8    new Annotation('choice', 'yes'),
9  ],
10  numericAnnotations: [new Annotation('weight', 1)],
11})) as ArkivCreate[];
12
13const receipts = await client.createEntities(extras);
14console.log(`Batch created: ${receipts.length} votes`);

6) Tally Votes

Read the chain back. Query annotated entities to compute the result. Because reads are deterministic, the same query yields the same answer.

  • Goal: Query votes by proposalKey and choice.
  • Success check: YES/NO counts match your inputs.
1const yesVotes = await client.queryEntities(
2  `type = "vote" && proposalKey = "${proposalKey}" && choice = "yes"`
3);
4const noVotes = await client.queryEntities(
5  `type = "vote" && proposalKey = "${proposalKey}" && choice = "no"`
6);
7console.log(`Tallies - YES: ${yesVotes.length}, NO: ${noVotes.length}`);

7) Watch Live

Subscribe to creations and extensions in real time. No polling-just logs as the story unfolds. Keep the same client; it already knows where to listen.

  • Goal: Subscribe to creation and extension events for votes (and proposals).
  • Success check: Console logs “[Vote created] …” or “[Vote extended] …”.
1const stopWatching = client.watchLogs({
2  fromBlock: BigInt(0),
3
4  onCreated: (e) => {
5    void (async () => {
6      try {
7        const meta = await (client as any).getEntityMetaData?.(e.entityKey);
8        const strs = Object.fromEntries((meta?.stringAnnotations ?? [])
9          .map((a: any) => [a.key, a.value]));
10        if (strs.type === 'vote') {
11          const data = await client.getStorageValue(e.entityKey);
12          console.log('[Vote created]', decoder.decode(data), 'key=', e.entityKey);
13        } else if (strs.type === 'proposal') {
14          const data = await client.getStorageValue(e.entityKey);
15          console.log('[Proposal created]', decoder.decode(data), 'key=', e.entityKey);
16        }
17      } catch {}
18    })();
19  },
20
21  onExtended: (e) => {
22    void (async () => {
23      try {
24        const meta = await (client as any).getEntityMetaData?.(e.entityKey);
25        const strs = Object.fromEntries((meta?.stringAnnotations ?? [])
26          .map((a: any) => [a.key, a.value]));
27        if (strs.type === 'vote') {
28          console.log('[Vote extended]', 'key=', e.entityKey, '→', e.newExpirationBlock);
29        } else if (strs.type === 'proposal') {
30          console.log('[Proposal extended]', 'key=', e.entityKey, '→', e.newExpirationBlock);
31        }
32      } catch {}
33    })();
34  },
35
36  onUpdated: () => {},
37  onDeleted: () => {},
38  onError: (err) => console.error('[watchLogs] error:', err),
39});
40console.log('Watching for proposal/vote creations and extensions…');

8) Extend Window

Need more time to decide? Extend the proposal’s Expires In. You’re updating the same entity you opened earlier-continuing the narrative of one decision from start to finish.

  • Goal: Extend the proposal entity by N blocks.
  • Success check: Console prints the new expiration block.
1const [ext] = await client.extendEntities([
2  { entityKey: proposal.entityKey, numberOfBlocks: 150 },
3]);
4console.log('Proposal extended to block:', ext.newExpirationBlock);

Setup & Installation

If you want to run this outside the browser (CI, local ts-node, a service), set up the SDK in your own project. This section shows package.json, .env and a reference script so you can run the same Voting Board flow from your terminal.

Prerequisites

SDK v0.1.19 (tested with arkiv-sdk@0.1.19). Requires Node.js 18 (LTS) or newer; verified on Node.js 20. Support for Bun 1.x runtime is available.

Node.js 18+ (or Bun 1.x)

LTS recommended

TypeScript 5+ (optional)

For typed scripts

Ethereum Wallet

With test ETH for your RPC

RPC Endpoint

HTTP + (optionally) WS

Arkiv Testnet "Kaolin" Resources

Arkiv Testnet Status

Installation

1# Using npm
2npm init -y
3npm i arkiv-sdk dotenv tslib ethers
4
5# or with Bun
6bun init -y
7bun add arkiv-sdk dotenv tslib ethers

tsconfig.json (optional)

1{
2  "compilerOptions": {
3    "target": "ES2022",
4    "module": "ESNext",
5    "moduleResolution": "Bundler",
6    "strict": true,
7    "esModuleInterop": true,
8    "skipLibCheck": true
9  },
10  "include": ["*.ts"]
11}

package.json (scripts)

1{
2  "type": "module",
3  "scripts": {
4    "start": "tsx voting-board.ts",
5    "build": "tsc",
6    "dev": "tsx watch voting-board.ts"
7  },
8  "dependencies": {
9    "arkiv-sdk": "^0.1.19",
10    "dotenv": "^16.4.5",
11    "tslib": "^2.8.1",
12    "ethers": "^6.13.4"
13  },
14  "devDependencies": {
15    "tsx": "^4.19.2",
16    "typescript": "^5.6.3"
17  }
18}

Environment Configuration

1# .env
2PRIVATE_KEY=0x...                      # use the (TEST) private key generated above
3RPC_URL=https://your.rpc.endpoint/rpc    # e.g. https://kaolin.hoodi.arkiv.network/rpc
4WS_URL=wss://your.rpc.endpoint/rpc/ws    # e.g. wss://kaolin.hoodi.arkiv.network/rpc/ws

Troubleshooting

Invalid sender: Your RPC may point to an unexpected network for your key. Verify your RPC URL is correct.

Insufficient funds: Get test ETH from the faucet; writes require gas.

No events seen? Ensure fromBlock is low enough and keep the process running to receive logs.