@arkiv-network/sdk
Version:
TypeScript client library for Arkiv's blockchains interactions
371 lines (361 loc) • 16 kB
text/typescript
import type { Account, Address, Chain, Client, Hex, PublicActions, Transport } from "viem"
import { type GetBlockTimingReturnType, getBlockTiming } from "../../actions/public/getBlockTiming"
import { getEntity } from "../../actions/public/getEntity"
import { getEntityNonce } from "../../actions/public/getEntityNonce"
import {
type PredictEntityKeysParameters,
type PredictEntityKeysReturnType,
predictEntityKeys,
} from "../../actions/public/predictEntityKeys"
import {
type QueryOptions,
type QueryReturnType,
type QueryRow,
query,
} from "../../actions/public/query"
import {
type WatchEntityEventsParameters,
watchEntityEvents,
} from "../../actions/public/watchEntityEvents"
import type { SaltInput } from "../../entity/key"
import { CountQueryBuilder } from "../../query/countBuilder"
import type { Expression } from "../../query/expression"
import type { OrderTerm } from "../../query/order"
import { SelectQueryBuilder } from "../../query/queryBuilder"
import type {
FullEntity,
FullRow,
KnownSystemFields,
NonEmptySelection,
PartialRow,
SelectArg,
SelectionOf,
} from "../../query/selection"
import type { Schema } from "../../schema"
export type PublicArkivActions<
transport extends Transport = Transport,
chain extends Chain | undefined = Chain | undefined,
account extends Account | undefined = Account | undefined,
> = Pick<
PublicActions<transport, chain, account>,
| "getBalance"
| "getBlock"
| "getBlockNumber"
| "getChainId"
| "getLogs"
| "getTransaction"
| "getTransactionCount"
| "getTransactionReceipt"
| "waitForTransactionReceipt"
| "watchEvent"
> & {
/**
* Returns the entity with the given key.
*
* Given a schema, it is read through it: the result is a flat row of every entity field and
* every column, each typed as the schema declares and checked against it.
*
* - JSON-RPC Methods: `arkiv_getEntity`
*
* @param key - The entity key (hex string)
* @param schema - Optionally, the schema to read the entity through.
* @returns The entity with the given key, with every field populated — a {@link FullEntity}, or
* a {@link FullRow} through a schema.
*
* @throws {InvalidValueError} If the key is not 32 bytes.
* @throws {NoEntityFoundError} If no live entity has that key — it never existed, or it was
* deleted or has expired.
* @throws {SchemaMismatchError} If, given a schema, the entity does not fit it.
* @throws {SchemaDefinitionError} If the schema given was not built with `defineSchema`.
*
* @example
* import { createPublicClient } from "@arkiv-network/sdk"
* import { rabdi } from "@arkiv-network/sdk/chains"
* import { http } from "viem"
*
* const client = createPublicClient({
* chain: rabdi({ accessKey: "..." }),
* transport: http(),
* })
* const entity = await client.getEntity(entityKey)
* // {
* // $key: "0x9f2c…",
* // $owner: "0xabc…",
* // $expiresAt: 1_297_000n,
* // $attributes: { category: { type: "str", value: "docs" } },
* // ...
* // }
*
* const post = await client.getEntity(entityKey, Posts)
* // { $key: "0x9f2c…", $owner: "0xabc…", ..., category: { type: "str", value: "docs" }, ... }
*/
getEntity: {
(key: Hex): Promise<FullEntity>
<S extends Schema>(key: Hex, schema: S): Promise<FullRow<S>>
}
/**
* Returns a SelectQueryBuilder for building and executing queries — the recommended way to
* read entities. You declare up front which parts of an entity you want returned, so results
* always contain exactly the data you asked for.
*
* The selection names entity fields and attributes side by side, and each result is a flat row.
* Chain `.from(schema)` to read through a schema: the selection keeps its meaning, but each
* attribute comes back typed and checked as the schema declares, and `where` may only filter on
* the schema's indexed columns.
*
* - JSON-RPC Methods: `arkiv_query`
*
* @param selection - What to include in the results. Omit it (or pass `"*"`) to select everything,
* or pass an object to select specific parts (at least one field is required). Every part is
* opt-in, including the `$key`. The selection is flat — each field maps to a field of the result.
* {@link SelectArg}
* @returns A SelectQueryBuilder instance for building and executing queries. {@link SelectQueryBuilder}
*
* @example
* import { createPublicClient } from "@arkiv-network/sdk"
* import { rabdi } from "@arkiv-network/sdk/chains"
* import { and, eq, gt } from "@arkiv-network/sdk/query"
* import { dec } from "@arkiv-network/sdk/schema"
* import { http } from "viem"
*
* const client = createPublicClient({
* chain: rabdi({ accessKey: "..." }),
* transport: http(),
* })
* // through a schema — rows typed { $key: Hex; author: AddrValue; title: StrValue }
* await client
* .select({ $key: true, author: true, title: true })
* .from(Posts)
* .where(and(eq("category", "docs"), gt("score", dec("4.0"))))
* .fetch()
* // every field and column
* await client.select().from(Posts).where(eq("category", "docs")).fetch()
*
* // the same selection without a schema — rows typed
* // { $key: Hex; author: ArkivValue | undefined; title: ArkivValue | undefined }
* await client.select({ $key: true, author: true, title: true }).where(eq("category", "docs")).fetch()
*/
select: {
/**
* Select every field — and every column through a schema, or the `$attributes` map without
* one. Pass nothing or `"*"`. The `$attributeSchema` is not included: select it by name.
*/
(selection?: "*"): SelectQueryBuilder<"*">
/**
* Pick the fields to return. Set the ones you want to `true` (at least one is required); the
* result is typed to exactly those fields, so reading anything else is a compile error.
*
* Entity fields start with `$`: `$key`, `$owner`, `$creator`, `$createdAt`, `$updatedAt`,
* `$expiresAt` and `$creationFlags`. `$attributes` reads every attribute as a map, and
* `$attributeSchema` their names, types and indexing without the values — a cheap way to find
* out what entities hold before fetching values that may be too large for one response. Any
* other name is an attribute, read as a field
* of its own: untyped without a schema, and through one typed and checked as the column it
* must be. An attribute name cannot start with `$`, so the two never collide.
*
* Pass the selection inline so its fields stay literal `true`. A selection stored in a `let`/
* `const` variable widens to `boolean`, and one typed {@link SelectArg} is only known at
* runtime: which fields come back is then decided at runtime, so every field of the result is
* optional — a {@link PartialRow}. Annotate a stored selection `as const` to keep it narrow.
*
* @example
* client.select({ $key: true, title: true }) // { $key: Hex; title: ArkivValue | undefined }
* client.select({ $key: true, title: true }).from(Posts) // { $key: Hex; title: StrValue }
* client.select({ $owner: true, $attributes: true }) // { $owner: Hex; $attributes: Attributes }
* client.select({ $key: true, $attributeSchema: true }) // what do they hold? no values
*/
<const S extends SelectArg>(
selection: S & NonEmptySelection<S> & KnownSystemFields<S>,
): SelectQueryBuilder<SelectionOf<S>>
}
/**
* Returns a CountQueryBuilder: counts the entities a query matches, without fetching any of them.
*
* It filters exactly as {@link select} does — `.from(schema)`, `.where(...)`, `.ownedBy(...)` and
* `.createdBy(...)` — and `.fetch()` resolves to a `bigint`. To show the total next to a page of
* rows, use `select(...).withCount()` instead.
*
* - JSON-RPC Methods: `arkiv_query`, with the `count` projection
*
* @example
* const total = await client.count().from(Posts).ownedBy(account.address).fetch()
* const news = await client.count().from(Posts).where(eq("category", "news")).fetch()
*/
count: () => CountQueryBuilder
/**
* Runs one query and returns one page, with no builder in between.
*
* The selection means what it does in {@link PublicArkivActions.select}, and the results are the
* same flat rows a query without a schema returns, typed from the selection. Use `select()` to
* read through a schema. This also takes a raw string as an escape hatch for a query built
* elsewhere. A raw string
* goes to the node exactly as written, with none of the name, type or operator checks the
* expression combinators apply.
*
* @param query - An {@link Expression}, or the raw query string.
* @param queryOptions - Selection, sort, page size, offset and count. {@link QueryOptions}
* @returns One page of entities. {@link QueryReturnType}
*
* @example
* import { createPublicClient } from "@arkiv-network/sdk"
* import { rabdi } from "@arkiv-network/sdk/chains"
* import { and, desc, eq, gte } from "@arkiv-network/sdk/query"
* import { i32 } from "@arkiv-network/sdk/schema"
* import { http } from "viem"
*
* const client = createPublicClient({
* chain: rabdi({ accessKey: "..." }),
* transport: http(),
* })
* const page = await client.query(and(eq("category", "docs"), gte("level", i32(10))), {
* select: { $key: true, title: true },
* orderBy: desc("level", "i32"),
* limit: 100,
* })
* // { entities: [{ $key: Hex; title: ArkivValue | undefined }], blockNumber: 32223n, count: undefined }
*
* // The raw form, unchecked:
* await client.query("category = str('docs') AND level >= i32(10)")
*/
query: <const S extends SelectArg = "*", const K extends OrderTerm = OrderTerm>(
query: Expression | string,
queryOptions?: QueryOptions<S, K>,
) => Promise<QueryReturnType<QueryRow<S>>>
/**
* Returns how many entities an account has created — its entity-minting nonce.
*
* Not the account's transaction nonce: the engine keeps its own counter per creator and mixes it
* into every key it mints, which is why two identical creates from one account never collide.
*
* @param owner - The account whose nonce to read.
* @returns The nonce the account's next create will use.
*
* @example
* import { createPublicClient } from "@arkiv-network/sdk"
* import { rabdi } from "@arkiv-network/sdk/chains"
* import { http } from "viem"
*
* const client = createPublicClient({
* chain: rabdi({ accessKey: "..." }),
* transport: http(),
* })
* const nonce = await client.getEntityNonce("0xabc…")
* // 7n — the next entity this account creates is its eighth
*/
getEntityNonce: (owner: Address) => Promise<bigint>
/**
* Works out the keys an account's next creates will be given, before sending them.
*
* The engine derives a key from the chain, the registry, the owner, the owner's nonce and a
* salt — everything but the nonce is known to the caller, and the nonce is read here. This is
* what lets a batch reference an entity it is about to mint.
*
* Each key arrives paired with the salt that mints it, because a create defaults to a *random*
* salt: a predicted key only holds if the create carries the salt it was predicted with. And the
* prediction holds only while nothing else from this owner is in flight — for a key you can rely
* on unconditionally, read it back from the create instead.
*
* A literal `count` comes back as a fixed-length tuple, so the pairs destructure into names.
*
* @param parameters - Owner, and either a count or the salts. {@link PredictEntityKeysParameters}
* @returns One `{ key, salt }` pair per create, in batch order.
* {@link PredictEntityKeysReturnType}
*
* @example A batch whose second entity points at its first.
* import { defineSchema, key, str } from "@arkiv-network/sdk/schema"
*
* const Nodes = defineSchema("nodes", { name: str(), parent: key().indexed().optional() })
*
* const [parent, child] = await client.predictEntityKeys({ owner: account.address, count: 2 })
* await wallet.executeBatch({
* creates: [
* { schema: Nodes, values: { name: "root" }, expiration, salt: parent.salt },
* {
* schema: Nodes,
* values: { name: "leaf", parent: key(parent.key) },
* expiration,
* salt: child.salt,
* },
* ],
* })
*/
predictEntityKeys: <
const TCount extends number = number,
const TSalts extends readonly SaltInput[] | undefined = undefined,
>(
parameters: PredictEntityKeysParameters<TCount, TSalts>,
) => Promise<PredictEntityKeysReturnType<TCount, TSalts>>
/**
* Returns the current block timing.
* @returns The current block timing. {@link GetBlockTimingReturnType}
*
* @example
* import { createPublicClient } from "@arkiv-network/sdk"
* import { rabdi } from "@arkiv-network/sdk/chains"
* import { http } from "viem"
*
* const client = createPublicClient({
* chain: rabdi({ accessKey: "..." }),
* transport: http(),
* })
* const blockTiming = await client.getBlockTiming()
* // {
* // currentBlock: 10n, // block number
* // currentBlockTime: 1234567890, // block timestamp
* // blockDuration: 2, // in seconds
* // }
*/
getBlockTiming: () => Promise<GetBlockTimingReturnType>
/**
* Watches entity events, calling the handlers you pass as they arrive.
*
* All five are decoded from logs — `onEntityCreated`, `onEntityPatched`, `onExpiryExtended`,
* `onOwnershipTransferred`, `onEntityDeleted` — and each carries the block, transaction and log
* index it came from, which is the order the operations were applied in. `onEvent` receives all
* of them, whatever their type.
*
* @param parameters - Handlers and options, all optional. {@link WatchEntityEventsParameters}
* @returns A function that stops the watcher.
*
* @example
* import { createPublicClient } from "@arkiv-network/sdk"
* import { rabdi } from "@arkiv-network/sdk/chains"
* import { http } from "viem"
*
* const client = createPublicClient({
* chain: rabdi({ accessKey: "..." }),
* transport: http(),
* })
* const unwatch = client.watchEntityEvents({
* onEntityCreated: ({ entityKey, expiresAt }) => console.log(entityKey, "until", expiresAt),
* onError: (error) => console.error("watchEntityEvents error", error),
* })
* unwatch() // stop watching
*/
watchEntityEvents: (parameters: WatchEntityEventsParameters) => () => void
}
export function publicArkivActions<
transport extends Transport = Transport,
chain extends Chain | undefined = Chain | undefined,
account extends Account | undefined = Account | undefined,
>(client: Client<transport, chain, account>) {
return {
getEntity: ((key: Hex, schema?: Schema) =>
getEntity(client, key, schema as Schema)) as PublicArkivActions["getEntity"],
query: ((rawQuery: Expression | string, queryOptions?: QueryOptions) =>
query(client, rawQuery, queryOptions)) as PublicArkivActions["query"],
select: ((selection?: SelectArg) =>
new SelectQueryBuilder(client, selection)) as PublicArkivActions["select"],
count: () => new CountQueryBuilder(client),
getBlockTiming: () => getBlockTiming(client),
getEntityNonce: (owner: Address) => getEntityNonce(client, owner),
predictEntityKeys: <
const TCount extends number = number,
const TSalts extends readonly SaltInput[] | undefined = undefined,
>(
parameters: PredictEntityKeysParameters<TCount, TSalts>,
) => predictEntityKeys(client, parameters),
watchEntityEvents: (parameters: WatchEntityEventsParameters) =>
watchEntityEvents(client, parameters),
}
}