@arkiv-network/sdk

中文文档
1.0.1发布于 2026/11/03TypeScriptESM + CJS零依赖构建来源已验证近 30 天下载 7401

TypeScript client library for Arkiv's blockchains interactions

原始使用文档
文档目录12 节
  1. Installation
  2. Usage
  3. Prerequisites
  4. Project Setup
  5. Define a Schema
  6. Create an Entity
  7. Query It
  8. Running the Example
  9. Package Distribution
  10. Runtime Support
  11. Development
  12. Verbose Logging

arkivjs

A TypeScript client library for Arkiv's blockchains interactions. The Arkiv SDK builds strongly on the Viem library - it extends Viem's clients with Arkiv's chain-specific features.

viem is a peer dependency: install it alongside the SDK.

Installation

npm install @arkiv-network/sdk viem
# or
pnpm install @arkiv-network/sdk viem
# or
bun add @arkiv-network/sdk viem
# or
yarn add @arkiv-network/sdk viem

Usage

Below is a tutorial to help you create simple scripts that use Arkiv to query and write data.

Prerequisites

  • Node.js 22.10.0 or newer (see nodejs.org), or bun, a JavaScript/TypeScript runtime and package manager that natively supports TypeScript without transpilation.
  • An Arkiv access key. Arkiv's public RPC is heavily rate limited to prevent abuse, so we recommend getting a free access key at hub.arkiv.network/access-keys.
  • Some testnet funds. Writing an entity is a transaction, and its gas is paid in testnet tGLM. Get some for your account's address from the faucet at hub.arkiv.network/faucet. Project Setup shows how to create an account if you don't have one.

Project Setup

Create a new directory and navigate into it:

mkdir arkiv-sample
cd arkiv-sample

Create an empty example.ts file:

touch example.ts

Initialize a new JavaScript/TypeScript project:

npm init

You can accept all the default options by pressing Enter at each prompt. After this step, a package.json file will be created with content similar to:

{
  "name": "arkiv-sample",
  "version": "1.0.0",
  "description": "",
  "license": "ISC",
  "author": "",
  "type": "commonjs",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  }
}

Modify the "main" entry to "example.ts" and set "type" to "module" so your package.json looks like this:

{
  "name": "arkiv-sample",
  "version": "1.0.0",
  "description": "",
  "license": "ISC",
  "author": "",
  "type": "module",
  "main": "example.ts",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  }
}

Install @arkiv-network/sdk along with its viem peer dependency using your preferred package manager:

npm install @arkiv-network/sdk viem

This command will update your package.json with a section like:

"dependencies": {
  "@arkiv-network/sdk": "^0.9.0",
  "viem": "^2.38.2"
}

It will also create a node_modules directory with all dependencies installed.

If you don't have a testnet account yet, create one with viem. This prints a new private key and its address:

node --input-type=module -e "import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; const key = generatePrivateKey(); console.log('Private key:', key); console.log('Address:', privateKeyToAccount(key).address)"

Fund the address at the faucet.

Finally, create a .env file with your access key and private key. The example reads them from there, so neither ends up in your code:

ARKIV_ACCESS_KEY=your-access-key
PRIVATE_KEY=0x-your-private-key

Define a Schema

Arkiv stores entities, and an entity is a set of named, typed attributes. You describe them once in a schema, much as you would describe a table's columns in SQL, and choose per column whether the node indexes it. Only indexed columns can be filtered on; the rest are stored and selectable, and do not pay for an index.

The examples below use this schema for blog posts. Each example declares it at the top of its own file, so that it runs as a single file under Node.js, Bun or tsc alike:

import { addr, bytes, dec, defineSchema, str } from "@arkiv-network/sdk/schema"

// Called with no arguments, a type constructor describes a column.
const Posts = defineSchema("posts", {
  author:    addr().indexed(),
  category:  str().indexed(),
  score:     dec().indexed(),
  title:     str(),                          // stored, not indexed
  body:      bytes(),                        // a blob; bytes can never be indexed
  thumbnail: bytes().optional(),             // any number of bytes columns; this one may be absent
  mimeType:  str().default("text/markdown"), // written on create when left out
})

The type constructors are bool, i32, u64, u256, dec, str, addr, key and bytes. Each column may be refined with .indexed() (every type but bytes), .optional() and .default(...).

The same constructors, called with a value, make a typed value: dec("4.1") is a decimal, the way SQL has DECIMAL as a type and DECIMAL '4.1' as a literal.

A schema is an SDK-side convention: the node sees only attributes, each with a type and an indexed flag. The schema is what types your writes and reads, and checks them at runtime.

The name, "posts", is optional but recommended. A named schema tags every entity it creates with an indexed arkiv.schema attribute holding its name, and a query through it only matches entities with that tag — the way a collection keeps one model's documents apart from another's. For that reason arkiv.schema cannot be used as a column name.

Create an Entity

Paste the following in example.ts. It sets up a public client, which reads, and a wallet client, which signs and sends transactions, then creates a post through the schema:

import { createPublicClient, createWalletClient, ExpirationTime } from "@arkiv-network/sdk"
import { rabdi } from "@arkiv-network/sdk/chains"
import { and, eq, gt } from "@arkiv-network/sdk/query"
import { addr, bytes, dec, defineSchema, str } from "@arkiv-network/sdk/schema"
import { hexToString, http, isHex, stringToBytes } from "viem"
import { privateKeyToAccount } from "viem/accounts"

// The schema from "Define a Schema" above
const Posts = defineSchema("posts", {
  author:    addr().indexed(),
  category:  str().indexed(),
  score:     dec().indexed(),
  title:     str(),
  body:      bytes(),
  thumbnail: bytes().optional(),
  mimeType:  str().default("text/markdown"),
})

// Rabdi is the Arkiv testnet
const chain = rabdi({ accessKey: process.env.ARKIV_ACCESS_KEY ?? "" })

const privateKey = process.env.PRIVATE_KEY
if (!privateKey || !isHex(privateKey)) {
  throw new Error("Set PRIVATE_KEY in .env to the private key of a funded testnet account")
}
const account = privateKeyToAccount(privateKey)

const publicClient = createPublicClient({ chain, transport: http() })
const walletClient = createWalletClient({ chain, transport: http(), account })

// Create an entity. Every required column must be given; a defaulted one (mimeType) left out is
// written with its default. Values carry their type: use the constructors from
// "@arkiv-network/sdk/schema", or a bare string, boolean, number, bigint or Uint8Array where it is
// unambiguous (string -> str, Uint8Array -> bytes, ...). createEntity resolves once the
// transaction is in a block.
const { entityKey, txHash } = await walletClient.createEntity(
  Posts,
  {
    author: addr(account.address),
    category: "docs",
    score: dec("4.5"),
    title: "Hello from Arkiv",
    body: stringToBytes("# Hello\n\nArkiv wishes you all the best!"),
  },
  { expiration: ExpirationTime.fromDays(1) }, // the entity expires in a day
)
console.log("Created entity:", entityKey)
console.log("Transaction hash:", txHash)

// Change one column, leaving the rest alone
await walletClient.patchEntity(Posts, entityKey, { set: { score: dec("4.7") } })

Query It

Now read the post back. Add the following to the end of example.ts:

// Get the entity by key, read through the schema
const post = await publicClient.getEntity(entityKey, Posts)
console.log("Title:", post.title.value, "score:", post.score.value)

// Or find it with a query: select what to return, name the schema, then filter
const result = await publicClient
  .select({ $key: true, title: true, score: true, body: true })
  .from(Posts)
  .where(and(eq("category", "docs"), gt("score", dec("4.0"))))
  .ownedBy(account.address)
  .limit(10)
  .fetch()

for (const row of result.entities) {
  console.log(row.$key, row.title.value, row.score.value, hexToString(row.body.value))
}

Selecting fields with select()

Pass nothing (or "*") to fetch everything — through a schema, every entity field and every column — or pass an object to fetch only specific fields. The selection names entity fields and attributes side by side, and each result is a flat row. Entity fields start with $, as they do in where: $key, $owner, $creator, $createdAt, $updatedAt, $expiresAt and $creationFlags. Any other name is an attribute. An attribute name cannot start with $, so the two never collide — an attribute called owner is just owner:

// Every entity field and every column
await publicClient.select().from(Posts).where(eq("category", "docs")).fetch();

// Only the fields you need
await publicClient.select({ $key: true, title: true }).from(Posts).where(eq("category", "docs")).fetch();

Select only what you need. Every selected field is fetched over the network, so requesting data you won't use makes queries slower. Narrowing the selection keeps responses small and fast.

The result type is inferred from your selection and schema: each column is typed as the schema declares it, and reading a field you didn't select is a compile error.

const row = (
  await publicClient.select({ $owner: true, score: true }).from(Posts).where(eq("category", "docs")).fetch()
).entities[0]!;
row.$owner;   // ✅ Hex
row.score;    // ✅ DecValue — row.score.value is "4.1"
row.title;    // ❌ compile error — not selected

where is checked against the schema too. It takes the schema's indexed columns and the system attributes $key, $owner, $creator and $expiresAt. Filtering on a column the schema does not index, or comparing a column against a value of another type, is a compile error:

.where(eq("title", "x"))   // ❌ title is not indexed
.where(eq("score", 5))     // ❌ score is a dec, 5 is an i32 — write dec(5)
.where(between("score", dec(1), 5))  // ❌ both bounds need one type

Sort with orderBy, passing one key: an asc(...) or a desc(...). Calling orderBy again replaces the order. The node sorts the attribute as one type, so the key has a type: through a schema it is the column's, and the system attributes $key, $owner, $creator and $expiresAt have fixed ones. Without a schema, write it. Call orderBy after from(...), so the compiler can check the key against the schema; a key given before it is checked only when the query is sent:

import { asc, desc } from "@arkiv-network/sdk/query"

.from(Posts).orderBy(desc("score"))      // score:dec DESC
.from(Posts).orderBy(asc("title"))       // title:str ASC, indexed or not
.from(Posts).orderBy(asc("$expiresAt"))  // $expiresAt:u64 ASC
.where(eq("category", "docs")).orderBy(desc("score", "dec"))  // without a schema
.from(Posts).orderBy(asc("body"))    // ❌ bytes cannot be sorted on
.from(Posts).orderBy(desc("$createdAt"))  // ❌ $createdAt cannot be sorted on
.from(Posts).orderBy(desc("score"), asc("$key"))  // ❌ one key only

Any column but a bytes one can be sorted on, indexed or not. An entity where the attribute is unset, or set with another type, has no value to sort by, and sorts below every value: first with asc, last with desc. The order holds across all the matches, not each page on its own.

Page with limit and offset. A page holds up to 100 rows, and offset skips the matches before it. Sort by a key that does not change, such as $key, so the pages line up:

const page3 = await publicClient
  .select({ $key: true, title: true })
  .from(Posts)
  .orderBy(asc("$key"))
  .limit(20)
  .offset(40)
  .fetch()

An entity created or deleted between two requests shifts the offsets after it, so a row can repeat or be missed. To read every match exactly once while others write, page by key instead: keep the asc("$key") sort and filter on gt("$key", key(lastKey)), where lastKey is the last $key of the previous page. Through a schema, pass { onMismatch: "throw" }: a page of skipped entities has no rows, so no key to continue from.

A named schema needs no where at all — select().from(Posts) matches every post. An unnamed schema types a query without filtering it, so where alone decides which entities match.

Only what you select is checked. An entity that matches but does not fit the schema — a selected column that is required but missing or holds another type, or, for a named schema, another schema's tag — is left out of the page. Anyone can write an entity a query matches, so one that does not fit never fails the read of the others. A page can then hold fewer rows than its limit, and reports how many it left out as skipped. Pass { onMismatch: "throw" } to fail the fetch with a SchemaMismatchError instead:

const page = await publicClient.select().from(Posts).where(eq("category", "docs")).fetch();
page.skipped; // matching entities on this page that did not fit Posts

A query reads the same without a schema — say, to explore what a node holds. Leave out .from(...) and the selection means what it did (save "*", below), and the rows keep their shape. Only the checking is gone, along with a named schema's tag filter: each attribute comes back as whatever value the entity holds, or undefined, and where takes any attribute:

const row = (
  await publicClient.select({ $key: true, title: true }).where(eq("category", "docs")).fetch()
).entities[0]!;
row.title;    // ArkivValue | undefined — e.g. { type: "str", value: "Hello" }

To read every attribute without naming them, select $attributes, a map of all of them. "*" (or nothing) fetches it alongside the entity fields when there is no schema, since there are no columns to list.

$attributeSchema is for finding out what entities hold without fetching their values: each attribute's name, its type, and whether it is indexed. Values can be large — a bytes attribute holds up to 128 KiB — and an RPC endpoint may cap how big a response it sends, so a page of 100 entities with every attribute can be more than it will return. Fetch the schema first, then select the attributes you need by name. It is never part of "*"; select it by name:

const page = await publicClient.select({ $key: true, $attributeSchema: true }).where(eq("category", "docs")).fetch();
page.entities[0]?.$attributeSchema; // { title: { type: "str", indexed: false }, body: { type: "bytes", indexed: false } }

Both work through a schema too.

Footgun: pass the selection inline. A selection stored in a variable widens its true values to boolean, so which fields come back is only known at runtime and every field of the result is optional. If you need to reuse one, annotate it as const:

const fields = { $owner: true, title: true } as const;
await publicClient.select(fields).from(Posts).where(eq("category", "docs")).fetch();

Running the Example

You have several options to run your TypeScript sample. Each loads your .env file:

  • With Node.js (using experimental TypeScript support):

    node --env-file=.env --experimental-strip-types example.ts
    
  • With Bun (native TypeScript support, loads .env on its own):

    bun example.ts
    
  • Classic Node.js (using transpilation to JavaScript):

    1. Install TypeScript and the Node.js type definitions if you haven't already:
      npm install typescript @types/node
      
    2. Initialize a TypeScript config with default settings:
      npx tsc --init
      
      This will create a tsconfig.json file. Set "types": ["node"] in it, so that process.env type-checks.
    3. Transpile your .ts files into .js:
      npx tsc --outDir dist
      
      This creates a dist directory containing example.js (the transpiled code), along with corresponding type declaration and source map files.
    4. Run the transpiled script:
      node --env-file=.env dist/example.js
      

Sample code can also be found in the sample directory of this repository.

Package Distribution

This package supports multiple module formats for maximum compatibility:

  • ES Modules (dist/*.js) - For modern import statements
  • CommonJS (dist/*.cjs) - For Node.js require()
  • Type Declarations (dist/*.d.ts and dist/*.d.cts) - Full TypeScript support

The build uses tsdown to generate both ESM and CommonJS formats with proper type declarations.

Runtime Support

Node.js (ESM):

import { createPublicClient } from '@arkiv-network/sdk';  // Uses compiled ESM

Node.js (CommonJS):

const { createPublicClient } = require('@arkiv-network/sdk');  // Uses compiled CJS

Bun (TypeScript native):

import { createPublicClient } from '@arkiv-network/sdk'; // Uses *.ts directly

All formats provide full type safety and IntelliSense support when using TypeScript.

Development

To install dependencies:

bun install

To build all outputs (ESM, CommonJS, and type declarations):

bun run build

For more information about building this SDK refer to: BUILD.md

To run type checking:

bun run type-check

To lint:

bun run lint

For more information about refer to: CONTRIBUTING.md

Verbose Logging

Set the DEBUG environment variable (or, in a browser, the DEBUG key in localStorage) to view verbose logs:

DEBUG=arkiv:* bun run your-script

Adjust the namespace (for example, arkiv:query:* or arkiv:actions:wallet:*) to target specific log sources, and prefix one with - to exclude it. Unset DEBUG to silence debug output.

版本与发布记录

@arkiv-network/sdk 共发布 100 个版本,首次发布于 2025/10/20;过去 12 个月发布了 76 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.0.1latest2026/11/035 天前
1.0.0-dev.2dev2026/11/035 天前
1.0.02026/10/308 天前
1.0.0-dev.12026/10/308 天前
0.9.0-next.5next2026/10/308 天前
0.9.0-next.32026/10/299 天前

其他发布通道:advanced → 0.8.0-advanced.3,next → 0.9.0-next.5,dev → 1.0.0-dev.2。

关于 @arkiv-network/sdk 的常见问题

@arkiv-network/sdk 是什么?

TypeScript client library for Arkiv's blockchains interactions

如何安装 @arkiv-network/sdk?

在项目目录运行 npm install @arkiv-network/sdk,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:viem(^2.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@arkiv-network/sdk 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@arkiv-network/sdk 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@arkiv-network/sdk 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@arkiv-network/sdk 可以商用吗?

@arkiv-network/sdk 使用 未声明 许可。package.json 没有声明许可证。没有许可证的代码默认保留全部权利,商用前请联系作者或查看仓库中的 LICENSE 文件。以上为概括,不构成法律意见,请以许可原文为准。

@arkiv-network/sdk 的下载量有多少?

根据 npm 官方下载统计,@arkiv-network/sdk 在 2026/10/07 – 2026/11/05 这 30 天内被下载 7,401 次,日均约 247 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@arkiv-network/sdk 还在维护吗?

最新版本 1.0.1 发布于 2026/11/03(5 天前);至今共发布 100 个版本,其中过去 12 个月发布了 76 个;首次发布于 2025/10/20。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。