@arkiv-network/sdk
Version:
TypeScript client library for Arkiv's blockchains interactions
899 lines (879 loc) • 40 kB
JavaScript
import { A as projectSelection, D as decodeCreationFlags, E as entityFromRpcResult, N as getLogger, O as encodeCreationFlags, S as runQuery, a as checkedOrderTerm, j as toRow, k as SYSTEM_FIELDS, r as CountQueryBuilder, s as renderOrderBy, t as SelectQueryBuilder } from "./query-LMk-y9L8.js";
import { a as MAX_LIFETIME, i as GAS_HEADROOM_PERCENT, n as ARKIV_ADDRESS, o as MAX_TRANSACTION_GAS, r as BLOCK_TIME } from "./consts-Lp55G1Qc.js";
import { i as NoEntityFoundError, n as EntityMutationError, r as MissingAccessKeyError, t as EmptyPatchError } from "./errors-CmOg0Na9.js";
import { A as str, B as DECIMAL_UNIT, C as bool, D as decUnits, E as decFromUnits, F as INDEXED_FLAG, G as MissingValueError, H as EntityTooLargeError, J as TooManyAttributesError, K as SchemaDefinitionError, L as isArkivValue, M as u256, N as u64, O as i32, P as isColumn, S as addr, T as dec, U as InvalidAttributeNameError, V as ConflictingMutationError, W as InvalidValueError, X as UntypedValueError, Y as UnknownAttributeTypeError, _ as I32_MIN, a as SCHEMA_ATTRIBUTE, b as U256_MAX, c as isSchema, d as isValidAttributeName, f as validateAttributeName, g as I32_MAX, h as decodeTypeId, i as MAX_ATTRIBUTES, j as toValue, k as key, l as lookupColumn, n as encodeCreate, o as defineSchema, q as SchemaMismatchError, r as encodePatch, s as describeColumns, t as MAX_ENTITY_BYTES, u as MAX_NAME_BYTES, v as MAX_BYTES_LENGTH, w as bytes, x as U64_MAX, y as MAX_STRING_BYTES, z as DECIMAL_SCALE } from "./schema-B-qj25Fz.js";
import { i as NoRandomSourceError, n as InvalidExpiryError, r as InvalidSaltError, t as InvalidCreationFlagsError } from "./errors-BLeoFbFG.js";
import { t as Entity } from "./entity-B5oCq0oX.js";
import { a as randomSalt, c as OperationType, i as predictEntityKey, n as MAX_SALT, o as resolveSalt, r as NO_SALT, s as MAX_EXPIRES_AT, t as ENTITY_EVENTS_ABI } from "./entity-DY0XB8so.js";
import { n as resolveExpiry, t as ExpirationTime } from "./expirationTime-C1gcuh7y.js";
import "./types-Bx1EKe0v.js";
import { BaseError, ContractFunctionExecutionError, ContractFunctionRevertedError, TransactionExecutionError, createClient, decodeEventLog, encodeAbiParameters, hexToString, parseAbi, parseAbiParameters, publicActions, walletActions } from "viem";
import { estimateContractGas, getChainId, getTransaction, readContract } from "viem/actions";
import { getAction } from "viem/utils";
//#region src/actions/public/getBlockTiming.ts
const logger$9 = getLogger("actions:public:get-block-timing");
async function getBlockTiming(client) {
const blockTiming = await client.request({
method: "arkiv_getBlockTiming",
params: []
});
logger$9("Block timing %o", blockTiming);
return {
currentBlock: BigInt(blockTiming.current_block),
currentBlockTime: blockTiming.current_block_time,
blockDuration: blockTiming.duration
};
}
//#endregion
//#region src/actions/public/getEntity.ts
async function getEntity(client, entityKey, schema) {
if (schema !== void 0 && !isSchema(schema)) throw new SchemaDefinitionError("getEntity() takes a schema built with defineSchema(...)");
const projection = projectSelection("*", schema);
const rpcEntity = await client.request({
method: "arkiv_getEntity",
params: [key(entityKey).value]
});
if (rpcEntity === null) throw new NoEntityFoundError(entityKey);
return toRow(entityFromRpcResult(rpcEntity), projection);
}
//#endregion
//#region src/entity/operations.ts
/**
* The one external entry point. A transaction is an ordered, **atomic** batch of entity operations:
* all of them apply or none do, operation *n+1* observes operation *n*'s effects, and each applied
* operation emits one event.
*/
const EXECUTE_ABI = parseAbi(["function execute((uint8 operation, bytes operationData)[] ops) external returns (bytes32[] keys)"]);
/**
* The engine's one read-only view: an owner's entity-minting nonce, which is the input
* {@link predictEntityKey} needs and the only part of the derivation the caller cannot know.
*
* It is a `view` on the same address `execute` is called on, so it answers over `eth_call` without
* a transaction.
*/
const ENTITY_NONCE_ABI = parseAbi(["function entityNonce(address owner) external view returns (uint64)"]);
/**
* The shared attribute shape, used by `create` (attributes) and `patch` (mutations). Written out
* once and reused by every payload that carries attributes.
*/
const ATTRIBUTE_TUPLE = "(bytes32 name, uint8 typeId, bytes value)[]";
/**
* The payload struct for each operation tag. `operationData` is `abi.encode` of the struct the tag
* selects — a tagged union, so a new operation is additive rather than a change to `execute`.
*/
const PAYLOAD_PARAMS = {
[OperationType.Create]: parseAbiParameters(`(uint128 salt, uint64 expiresAt, uint64 minLifetime, uint8 creationFlags, ${ATTRIBUTE_TUPLE} attributes)`),
[OperationType.Patch]: parseAbiParameters(`(bytes32 entityKey, ${ATTRIBUTE_TUPLE} mutations)`),
[OperationType.ExtendExpiry]: parseAbiParameters("(bytes32 entityKey, uint64 expiresAt, uint64 minLifetime)"),
[OperationType.TransferOwnership]: parseAbiParameters("(bytes32 entityKey, address newOwner)"),
[OperationType.Delete]: parseAbiParameters("(bytes32 entityKey)")
};
/** Creates an entity. The key is derived by the engine; predict it with `predictEntityKey`. */
function createOperation({ salt, expiry, creationFlags, attributes }) {
return {
operation: OperationType.Create,
operationData: encodeAbiParameters(PAYLOAD_PARAMS[OperationType.Create], [{
salt,
expiresAt: expiry.expiresAt,
minLifetime: expiry.minLifetime,
creationFlags,
attributes
}])
};
}
/** Applies a set of mutations to an existing entity. Tombstones unset attributes. */
function patchOperation({ entityKey, mutations }) {
return {
operation: OperationType.Patch,
operationData: encodeAbiParameters(PAYLOAD_PARAMS[OperationType.Patch], [{
entityKey,
mutations
}])
};
}
/** Pushes an entity's expiry further out. Extending is allowed; shortening reverts. */
function extendExpiryOperation({ entityKey, expiry }) {
return {
operation: OperationType.ExtendExpiry,
operationData: encodeAbiParameters(PAYLOAD_PARAMS[OperationType.ExtendExpiry], [{
entityKey,
expiresAt: expiry.expiresAt,
minLifetime: expiry.minLifetime
}])
};
}
/** Hands an entity to a new owner. */
function transferOwnershipOperation({ entityKey, newOwner }) {
return {
operation: OperationType.TransferOwnership,
operationData: encodeAbiParameters(PAYLOAD_PARAMS[OperationType.TransferOwnership], [{
entityKey,
newOwner
}])
};
}
/** Deletes an entity before its expiry. */
function deleteOperation({ entityKey }) {
return {
operation: OperationType.Delete,
operationData: encodeAbiParameters(PAYLOAD_PARAMS[OperationType.Delete], [{ entityKey }])
};
}
//#endregion
//#region src/actions/public/getEntityNonce.ts
const logger$8 = getLogger("actions:public:get-entity-nonce");
/**
* Reads an owner's entity-minting nonce — how many entities that account has created.
*
* This is not the account's transaction nonce. The engine keeps a separate counter per creator and
* mixes it into every key it mints, which is what makes two identical creates from one account land
* on different keys.
*
* @param owner - The account whose nonce to read.
* @returns The number of entities `owner` has created so far, which is the nonce its next create
* will use.
*/
async function getEntityNonce(client, owner) {
const nonce = await readContract(client, {
address: ARKIV_ADDRESS,
abi: ENTITY_NONCE_ABI,
functionName: "entityNonce",
args: [owner]
});
logger$8("entity nonce of %s is %s", owner, nonce);
return nonce;
}
//#endregion
//#region src/actions/public/predictEntityKeys.ts
/**
* Works out the keys an account's next creates will be given, before sending them.
*
* A key is `keccak256(chainId ++ registry ++ owner ++ nonce ++ salt)`. Everything but the nonce is
* known to the caller, and the nonce is read here — so a batch can reference a key it is about to
* mint, storing it as a `key` attribute on a sibling entity created in the same transaction.
*
* Each key arrives paired with its salt, and the create must be given that salt: a create left to
* pick its own salt picks a random one, which mints a different key.
*
* The prediction is valid only while nothing else from this owner is in flight. Every create that
* lands first takes the nonce with it and moves these keys. For a key you can rely on
* unconditionally, read it back from the create instead — `createEntity` and `executeBatch`
* report the keys the engine actually minted.
*
* @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,
* },
* ],
* })
*/
async function predictEntityKeys(client, { owner, count, salts }) {
const resolvedSalts = resolveSalts(count, salts);
const chainId = client.chain?.id ?? await getChainId(client);
const nonce = await getEntityNonce(client, owner);
return resolvedSalts.map((salt, index) => ({
key: predictEntityKey({
owner,
nonce: nonce + BigInt(index),
salt,
chainId
}),
salt
}));
}
/** The salts to derive from: the ones given, or `count` fresh random ones. */
function resolveSalts(count, salts) {
if (salts !== void 0) {
if (salts.length === 0) throw new Error("predictEntityKeys: `salts` is empty — there is nothing to predict");
return salts.map(resolveSalt);
}
if (count === void 0) throw new Error("predictEntityKeys: pass either `count` or `salts`");
if (!Number.isInteger(count) || count <= 0) throw new Error(`predictEntityKeys: \`count\` must be a positive whole number, got ${count}`);
return Array.from({ length: count }, () => randomSalt());
}
//#endregion
//#region src/actions/public/query.ts
/**
* Runs a single query and returns one page, with no builder in between.
*
* The selection means what it does in `select()`, and each result is the same flat row a query
* without a schema returns — typed from the selection, with every attribute an `ArkivValue` or
* `undefined`.
*
* Takes either an {@link Expression} or a raw query string. The string form is an escape hatch for
* a query built elsewhere — it goes to the node exactly as written, with none of the name, type or
* operator checks the expression combinators apply. An `orderBy` is checked either way, and goes
* after the string as its `ORDER BY` clause, so a raw string must not end in one of its own.
*
* @throws If the selection asks for nothing, or names an attribute outside the name grammar.
* @throws If `limit` is not an integer from 1 to 100, or `offset` is not a non-negative integer.
* @throws {InvalidPredicateError} If `orderBy` is not a key `asc` or `desc` would build, or names a
* user attribute without its type.
* @throws {QueryError} If the node rejects the query.
*/
async function query(client, query$1, options = {}) {
const projection = projectSelection(options.select);
const filters = query$1.toString();
const order = options.orderBy === void 0 ? void 0 : renderOrderBy(checkedOrderTerm(options.orderBy), void 0);
const page = await runQuery(client, {
query: order === void 0 ? filters : `${filters} ${order}`,
select: projection.select,
limit: options.limit,
offset: options.offset,
count: options.count
});
return {
...page,
entities: page.entities.map((entity) => toRow(entity, projection))
};
}
//#endregion
//#region src/actions/public/watchEntityEvents.ts
const logger$7 = getLogger("actions:public:watch-entity-events");
/**
* How often to poll, when the caller does not say.
*
* Half a block: fast enough that an event is never a whole block stale, slow enough not to hammer
* the node. viem's own default is derived from a 12-second Ethereum block and is far too slow here.
*/
const DEFAULT_POLLING_INTERVAL = BLOCK_TIME * 1e3 / 2;
/**
* Watches entity events, calling the handlers you pass as they arrive.
*
* The watcher follows the head. It starts listening on its first poll, which sets up the filter on
* the node, so an event mined between the call and that poll — up to one polling interval — is not
* delivered, and nor is anything earlier: the network serves no historical reads. To build state
* from scratch, start the watcher first and then query the current state. The two overlap, so
* apply events in a way that tolerates seeing a change the query already returned.
*
* Every event here is decoded from a log the Arkiv operation address emitted, and carries the
* block, transaction and log index it came from — so consuming them in order replays exactly what
* the engine did.
*
* @param parameters - Handlers and options. {@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, owner, expiresAt }) =>
* console.log("created", entityKey, "by", owner, "until block", expiresAt),
* onEntityDeleted: ({ entityKey }) => console.log("deleted", entityKey),
* onError: (error) => console.error(error),
* })
*
* unwatch()
*/
function watchEntityEvents(client, parameters) {
const { onEntityCreated, onEntityPatched, onExpiryExtended, onOwnershipTransferred, onEntityDeleted, onEvent, onError, pollingInterval = DEFAULT_POLLING_INTERVAL } = parameters;
const publicClient = client;
const reportError = onError ?? logToConsole;
function emit(event) {
deliver(onEvent, event);
switch (event.type) {
case "EntityCreated":
deliver(onEntityCreated, event);
break;
case "EntityPatched":
deliver(onEntityPatched, event);
break;
case "ExpiryExtended":
deliver(onExpiryExtended, event);
break;
case "OwnershipTransferred":
deliver(onOwnershipTransferred, event);
break;
case "EntityDeleted":
deliver(onEntityDeleted, event);
break;
}
}
/**
* Runs one handler, keeping its failure to itself.
*
* A handler that throws is the consumer's problem, not the watcher's: letting it escape would
* reject the poll, drop the rest of the batch, and take down every other handler with it.
*/
function deliver(handler, event) {
if (handler === void 0) return;
try {
handler(event);
} catch (error) {
reportError(asError(error));
}
}
return publicClient.watchEvent({
address: ARKIV_ADDRESS,
events: ENTITY_EVENTS_ABI,
pollingInterval,
onLogs: (logs) => {
logger$7("logs %o", logs);
for (const log of logs) {
if (log.blockNumber === null || log.transactionHash === null || log.logIndex === null) continue;
const context = {
blockNumber: log.blockNumber,
transactionHash: log.transactionHash,
logIndex: log.logIndex
};
let event;
try {
event = toEntityEvent(log.topics, log.data, context);
} catch (error) {
reportError(asError(error));
continue;
}
emit(event);
}
},
onError: (error) => reportError(error)
});
}
/** The fallback when no `onError` was given — see {@link WatchEntityEventsParameters.onError}. */
function logToConsole(error) {
console.error("watchEntityEvents error", error);
}
/** Handlers take an `Error`, and a `throw` can be anything. */
function asError(error) {
return error instanceof Error ? error : new Error(String(error));
}
/**
* Decodes one log into the {@link EntityEvent} it carries.
*
* @throws If the log does not decode against {@link ENTITY_EVENTS_ABI}.
*/
function toEntityEvent(topics, data, context) {
const decoded = decodeEventLog({
abi: ENTITY_EVENTS_ABI,
topics,
data
});
switch (decoded.eventName) {
case "EntityCreated": return {
type: "EntityCreated",
...context,
...decoded.args,
creationFlags: decodeCreationFlags(decoded.args.creationFlags)
};
case "EntityPatched": return {
type: "EntityPatched",
...context,
...decoded.args
};
case "ExpiryExtended": return {
type: "ExpiryExtended",
...context,
...decoded.args
};
case "OwnershipTransferred": return {
type: "OwnershipTransferred",
...context,
...decoded.args
};
case "EntityDeleted": return {
type: "EntityDeleted",
...context,
...decoded.args
};
}
}
//#endregion
//#region src/clients/decorators/arkivPublic.ts
function publicArkivActions(client) {
return {
getEntity: ((key$1, schema) => getEntity(client, key$1, schema)),
query: ((rawQuery, queryOptions) => query(client, rawQuery, queryOptions)),
select: ((selection) => new SelectQueryBuilder(client, selection)),
count: () => new CountQueryBuilder(client),
getBlockTiming: () => getBlockTiming(client),
getEntityNonce: (owner) => getEntityNonce(client, owner),
predictEntityKeys: (parameters) => predictEntityKeys(client, parameters),
watchEntityEvents: (parameters) => watchEntityEvents(client, parameters)
};
}
//#endregion
//#region src/clients/createPublicClient.ts
/**
* Creates a Public Client with a given [Transport](https://viem.sh/docs/clients/intro) configured for a [Chain](https://viem.sh/docs/chains/introduction).
*
* A Public Client is an interface to the "public" [Ethereum JSON-RPC API](https://ethereum.org/en/developers/docs/apis/json-rpc/)
* and the Arkiv JSON-RPC API — retrieving block numbers and transactions, reading from
* contracts, and querying entities — through {@link PublicArkivActions}.
*
* @param parameters - Configuration object for the public client (chain, transport, etc.)
* @returns An Arkiv Public Client. {@link PublicArkivClient}
*
* @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(),
* })
*/
function createPublicClient(parameters) {
const { key: key$1 = "public", name = "Public Client" } = parameters;
return createClient({
...parameters,
key: key$1,
name
}).extend(publicActions).extend(publicArkivActions);
}
//#endregion
//#region src/utils/revert.ts
/**
* Turns a decoded engine revert into a sentence that says what to change.
*
* viem decodes the error and then renders it positionally — `Ident32InvalidByte(uint256 position,
* bytes1 value)` arrives as `(5, 0x41)`, which is the right data and no help at all. The engine's
* errors carry enough to name the actual problem, so this spends the args rather than printing
* them.
*
* @param error - Anything thrown by a write action.
* @returns The explanation, or `undefined` if this was not a decodable engine revert — in which
* case the caller should fall back to whatever the transport said.
*/
function describeEntityRevert(error) {
if (!(error instanceof BaseError)) return void 0;
const reverted = error.walk((e) => e instanceof ContractFunctionRevertedError);
if (!(reverted instanceof ContractFunctionRevertedError)) return void 0;
const name = reverted.data?.errorName;
if (name === void 0) return void 0;
return explain(name, reverted.data?.args ?? []) ?? `the engine rejected the batch with ${name}`;
}
/** The attribute-name charset, quoted the way an error message should read. */
const CHARSET = "\"A\"-\"Z\", \"a\"-\"z\", \"0\"-\"9\", \".\", \"-\" and \"_\", with a letter first";
function explain(name, args) {
switch (name) {
case "Ident32InvalidByte": {
const position = Number(args[0] ?? 0);
return `an attribute name holds ${printable(String(args[1] ?? "0x00"))} at byte ${position}, which is outside the name charset (${CHARSET})`;
}
case "Ident32Empty": return "an attribute name is empty";
case "SystemAttributeNotWritable": return `"${identName(args[0])}" is owned by the engine and cannot be written. Names starting with "\$" are reserved for system attributes`;
case "TombstoneInCreate": return `a create cannot unset "${identName(args[0])}" — nothing exists yet, so leave the name out instead`;
case "TombstoneValueNotEmpty": return `the tombstone for "${identName(args[0])}" carries a value; unsetting takes no value`;
case "InvalidValueType": return describeInvalidType(identName(args[0]), Number(args[1]));
case "AttributesNotSorted": return "attributes must be strictly ascending by name, which also makes each name unique";
case "TooManyAttributes": return `${String(args[0])} attributes exceeds the limit of ${String(args[1])}`;
case "EmptyMutations": return `the patch of ${short(args[0])} has no mutations, so it would do nothing`;
case "EntityNotFound": return `no entity ${short(args[0])} — it may have been deleted, or have expired`;
case "EntityExpired": return `entity ${short(args[0])} expired at block ${String(args[1])}`;
case "NotOwner": return `entity ${short(args[0])} is owned by ${String(args[2])}, not ${String(args[1])}`;
case "ReadOnlyEntity": return `entity ${short(args[0])} was created readonly, so its contents can never change. Its expiry can still be extended, and it can be transferred or deleted`;
case "ReservedCreationFlags": return `creation flags 0b${Number(args[0] ?? 0).toString(2).padStart(8, "0")} set a reserved bit; only readonly and permissionlessExtension are defined`;
case "ExpiryDeadOnArrival": return `the expiry resolves to block ${String(args[0])}, which is not after the current block ${String(args[1])}`;
case "ExpiryBeyondMaxLifetime": return `the expiry resolves to block ${String(args[0])}, more than the chain's maximum lifetime of ${String(args[2])} blocks after the current block ${String(args[1])}`;
case "ExpiryNotExtended": return `entity ${short(args[0])} already expires at block ${String(args[2])}, so extending it to ${String(args[1])} would shorten its life`;
case "TransferToZeroAddress": return `entity ${short(args[0])} cannot be transferred to the zero address`;
case "TransferToSelf": return `entity ${short(args[0])} is already owned by that account`;
case "EmptyBatch": return "the batch has no operations";
case "InvalidOpType": return `operation tag ${String(args[0])} is not one the engine knows`;
case "NonCanonicalOperationData": return `the payload for operation tag ${String(args[0])} is not its canonical encoding, so the engine refused it rather than accept a second spelling of the same operation`;
default: return;
}
}
/**
* Why the engine refused a `typeId` — an indexed `bytes`, a known type it would not take (most
* likely the indexed flag, on a node that predates it), or a type it does not know.
*/
function describeInvalidType(name, typeId) {
try {
const { tag, indexed } = decodeTypeId(typeId);
if (tag === "bytes" && indexed) return `"${name}" is a bytes attribute flagged as indexed, and bytes are stored but never indexed`;
if (indexed) return `"${name}" has type ${tag} and is flagged as indexed (type id ${typeId}), which the node does not accept. The node may not support the indexed flag yet`;
return `"${name}" has type ${tag} (type id ${typeId}), which the node does not accept`;
} catch {}
return `"${name}" names type id ${typeId}, which is not a known type`;
}
/** A `bytes32` Ident32 cell as the name it holds — left-aligned, null-padded. */
function identName(raw) {
if (typeof raw !== "string") return String(raw);
try {
return hexToString(raw).split("\0")[0] ?? "";
} catch {
return raw;
}
}
/** An entity key, abbreviated */
function short(raw) {
const text = String(raw);
return text.length > 18 ? `${text.slice(0, 10)}…${text.slice(-6)}` : text;
}
/** A `bytes1` as something readable: the character it is, plus the byte it was. */
function printable(byte) {
const code = Number.parseInt(byte.replace(/^0x/, ""), 16);
if (!Number.isFinite(code)) return `byte ${byte}`;
return code >= 33 && code <= 126 ? `"${String.fromCharCode(code)}" (${byte})` : `byte ${byte}`;
}
//#endregion
//#region src/utils/arkivTransactions.ts
const logger$6 = getLogger("utils:arkiv-transactions");
const ENTITY_ERRORS_ABI = parseAbi([
"error EmptyBatch()",
"error InvalidOpType(uint8 operation)",
"error NonCanonicalOperationData(uint8 operation)",
"error AttributesNotSorted()",
"error TooManyAttributes(uint256 count, uint256 maxCount)",
"error EmptyMutations(bytes32 entityKey)",
"error TombstoneInCreate(bytes32 name)",
"error TombstoneValueNotEmpty(bytes32 name)",
"error SystemAttributeNotWritable(bytes32 name)",
"error InvalidValueType(bytes32 name, uint8 typeId)",
"error Ident32Empty()",
"error Ident32InvalidByte(uint256 position, bytes1 value)",
"error EntityNotFound(bytes32 entityKey)",
"error EntityExpired(bytes32 entityKey, uint64 expiresAt)",
"error NotOwner(bytes32 entityKey, address caller, address owner)",
"error ReadOnlyEntity(bytes32 entityKey)",
"error ReservedCreationFlags(uint8 creationFlags)",
"error ExpiryDeadOnArrival(uint64 target, uint64 currentBlock)",
"error ExpiryBeyondMaxLifetime(uint64 target, uint64 currentBlock, uint64 maxLifetime)",
"error ExpiryNotExtended(bytes32 entityKey, uint64 newExpiresAt, uint64 currentExpiresAt)",
"error TransferToZeroAddress(bytes32 entityKey)",
"error TransferToSelf(bytes32 entityKey)"
]);
const EXECUTE_ABI$1 = [...EXECUTE_ABI, ...ENTITY_ERRORS_ABI];
async function sendArkivTransaction(client, ops, txParams) {
if (!client.account) throw new Error("Account required");
if (!client.chain) throw new Error("Chain required");
const walletClient = client;
const { creates, patches, deletes, extensions, ownershipChanges } = ops;
const encodedCreates = (creates ?? []).map((item) => ({
item,
attributes: encodeCreate(item.schema, item.values)
}));
const patchOps = (patches ?? []).map((item) => {
const mutations = encodePatch(item.schema, item);
if (mutations.length === 0) throw new EmptyPatchError(item.entityKey);
return patchOperation({
entityKey: item.entityKey,
mutations
});
});
const expiryContext = { currentBlock: creates?.length || extensions?.length ? await walletClient.getBlockNumber() : 0n };
const createOps = encodedCreates.map(({ item, attributes }) => {
return createOperation({
salt: item.salt === void 0 ? randomSalt() : resolveSalt(item.salt),
expiry: resolveExpiry(item.expiration, expiryContext),
creationFlags: encodeCreationFlags(item.flags),
attributes
});
});
const extendOps = (extensions ?? []).map((item) => {
const expiry = resolveExpiry(item.expiration, expiryContext);
return extendExpiryOperation({
entityKey: item.entityKey,
expiry
});
});
const operations = [
...createOps,
...patchOps,
...(deletes ?? []).map((item) => deleteOperation({ entityKey: item.entityKey })),
...extendOps,
...(ownershipChanges ?? []).map((item) => transferOwnershipOperation({
entityKey: item.entityKey,
newOwner: item.newOwner
}))
];
if (operations.length === 0) throw new Error("No operations to perform");
logger$6("Sending execute with %d operations %o", operations.length, operations);
let txHash;
try {
const gas = txParams?.gas ?? withHeadroom(await estimateContractGas(walletClient, {
address: ARKIV_ADDRESS,
abi: EXECUTE_ABI$1,
functionName: "execute",
args: [operations],
account: {
address: client.account.address,
type: "json-rpc"
},
...txParams
}));
txHash = await walletClient.writeContract({
address: ARKIV_ADDRESS,
abi: EXECUTE_ABI$1,
functionName: "execute",
args: [operations],
account: client.account,
chain: client.chain,
...txParams,
gas
});
const receipt = await walletClient.waitForTransactionReceipt({ hash: txHash });
logger$6("Tx receipt %o", receipt);
if (receipt.status === "reverted") {
const actualLimitUsed = await getGasLimitOfTx(walletClient, txHash) ?? gas;
if (receipt.gasUsed >= actualLimitUsed) {
const estimated = txParams?.gas === void 0 && actualLimitUsed === gas;
throw new EntityMutationError(`Transaction ${receipt.transactionHash} ran out of gas: it used all ${actualLimitUsed} gas it was given, so nothing was applied and the fee was spent. ` + (estimated ? "The batch cost more when it ran than when it was estimated, because the state changed in between. Send it again." : "The limit was set rather than estimated (passed as `gas`, or changed in the wallet) and was too low for the batch. Send it again with a higher `gas`, or without one to let the SDK estimate it."), { txHash: receipt.transactionHash });
}
try {
await walletClient.simulateContract({
address: ARKIV_ADDRESS,
abi: EXECUTE_ABI$1,
functionName: "execute",
args: [operations],
account: client.account,
chain: client.chain
});
} catch (err) {
const error = err;
const reason = describeEntityRevert(err) ?? error.shortMessage ?? error.cause?.details ?? "No reason provided by backend.";
throw new EntityMutationError(`Transaction ${receipt.transactionHash} reverted. Reason: ${reason}`, {
cause: err,
txHash: receipt.transactionHash
});
}
throw new EntityMutationError(`Transaction ${receipt.transactionHash} reverted. No reason provided by backend.`, { txHash: receipt.transactionHash });
}
return {
receipt,
...readAppliedOperations(receipt, {
creates: createOps.length,
extensions: extendOps.length
})
};
} catch (error) {
const described = error instanceof EntityMutationError ? void 0 : describeEntityRevert(error);
if (described !== void 0) throw new EntityMutationError(`Transaction failed: ${described}`, {
cause: error,
txHash
});
let message = "Transaction failed";
if (error instanceof TransactionExecutionError) message += `: ${error.details}`;
else if (error instanceof ContractFunctionExecutionError) {
logger$6("Contract function execution error data:", error.shortMessage);
if (error.cause instanceof ContractFunctionRevertedError) message += `: ${error.cause.message}`;
else message += ": Execution error without revert data";
} else if (error instanceof EntityMutationError) throw error;
else if (error instanceof Error) message += `: ${error.message}`;
throw new EntityMutationError(message, {
cause: error,
txHash
});
}
}
/** The gas limit a sent transaction carried, or `undefined` if the node cannot say. */
async function getGasLimitOfTx(client, hash) {
try {
return (await getAction(client, getTransaction, "getTransaction")({ hash })).gas;
} catch {
return;
}
}
/**
* An estimate with {@link GAS_HEADROOM_PERCENT} added: room for the state to change between the
* estimate and the block the transaction lands in. Capped at {@link MAX_TRANSACTION_GAS}, so the
* headroom alone never makes a batch too big to send, but never below the estimate itself.
*/
function withHeadroom(estimate) {
if (estimate > MAX_TRANSACTION_GAS) return estimate;
const padded = estimate * (100n + GAS_HEADROOM_PERCENT) / 100n;
if (padded > MAX_TRANSACTION_GAS) return MAX_TRANSACTION_GAS;
return padded;
}
/**
* The keys and expiries the engine recorded, read back from the events the batch emitted.
*/
function readAppliedOperations(receipt, expected) {
const createdEntityKeys = [];
const createdExpiries = [];
const extendedExpiries = [];
for (const log of receipt.logs ?? []) {
if (log.address.toLowerCase() !== ARKIV_ADDRESS.toLowerCase()) continue;
const decoded = decodeEntityLog(log);
if (decoded === void 0) continue;
if (decoded.eventName === "EntityCreated") {
createdEntityKeys.push(decoded.args.entityKey);
createdExpiries.push(decoded.args.expiresAt);
} else if (decoded.eventName === "ExpiryExtended") extendedExpiries.push(decoded.args.expiresAt);
}
assertCount(receipt, "create", createdEntityKeys.length, expected.creates);
assertCount(receipt, "extension", extendedExpiries.length, expected.extensions);
return {
createdEntityKeys,
createdExpiries,
extendedExpiries
};
}
/** One log as an entity event, or `undefined` for anything this SDK has no name for. */
function decodeEntityLog(log) {
try {
return decodeEventLog({
abi: ENTITY_EVENTS_ABI,
topics: log.topics,
data: log.data
});
} catch {
return;
}
}
function assertCount(receipt, what, got, want) {
if (got === want) return;
throw new EntityMutationError(`Transaction ${receipt.transactionHash} succeeded — do not retry it — but emitted ${got} ${what} event(s) for ${want} ${what} operation(s). The batch was applied; the SDK cannot say which entity each operation produced, so read them back to find them.`, { txHash: receipt.transactionHash });
}
//#endregion
//#region src/actions/wallet/changeOwnership.ts
const logger$5 = getLogger("actions:wallet:change-ownership");
async function changeOwnership(client, data, txParams) {
logger$5("changeOwnership %o", data);
const { receipt } = await sendArkivTransaction(client, { ownershipChanges: [data] }, txParams);
logger$5("Receipt from changeOwnership %o", receipt);
return {
txHash: receipt.transactionHash,
entityKey: data.entityKey
};
}
//#endregion
//#region src/actions/wallet/createEntity.ts
const logger$4 = getLogger("actions:wallet:create-entity");
async function createEntity(client, schema, values, options, txParams) {
logger$4("createEntity %o", values);
const { receipt, createdEntityKeys, createdExpiries } = await sendArkivTransaction(client, { creates: [{
...options,
schema,
values
}] }, txParams);
logger$4("Receipt from createEntity %o", receipt);
const [entityKey] = createdEntityKeys;
const [expiresAt] = createdExpiries;
if (entityKey === void 0 || expiresAt === void 0) throw new EntityMutationError(`Transaction ${receipt.transactionHash} succeeded, but the receipt did not carry the expected entity key or expiry.`, { txHash: receipt.transactionHash });
return {
txHash: receipt.transactionHash,
entityKey,
expiresAt
};
}
//#endregion
//#region src/actions/wallet/deleteEntity.ts
const logger$3 = getLogger("actions:wallet:delete-entity");
async function deleteEntity(client, data, txParams) {
logger$3("deleteEntity %o", data);
const { receipt } = await sendArkivTransaction(client, { deletes: [data] }, txParams);
logger$3("Receipt from deleteEntity %o", receipt);
return {
txHash: receipt.transactionHash,
entityKey: data.entityKey
};
}
//#endregion
//#region src/actions/wallet/executeBatch.ts
const logger$2 = getLogger("actions:wallet:execute-batch");
async function executeBatch(client, data, txParams) {
const { receipt, createdEntityKeys } = await sendArkivTransaction(client, data, txParams);
logger$2("Receipt from executeBatch %o", receipt);
return {
txHash: receipt.transactionHash,
createdEntities: createdEntityKeys,
patchedEntities: (data.patches ?? []).map((p) => p.entityKey),
deletedEntities: (data.deletes ?? []).map((d) => d.entityKey),
extendedEntities: (data.extensions ?? []).map((e) => e.entityKey),
ownershipChanges: (data.ownershipChanges ?? []).map((o) => o.entityKey)
};
}
//#endregion
//#region src/actions/wallet/extendEntity.ts
const logger$1 = getLogger("actions:wallet:extend-entity");
async function extendEntity(client, data, txParams) {
logger$1("extendEntity %o", data);
const { receipt, extendedExpiries } = await sendArkivTransaction(client, { extensions: [data] }, txParams);
logger$1("Receipt from extendEntity %o", receipt);
const [expiresAt] = extendedExpiries;
if (expiresAt === void 0) throw new EntityMutationError(`Transaction ${receipt.transactionHash} succeeded, but the receipt did not carry the expected entity expiry.`, { txHash: receipt.transactionHash });
return {
txHash: receipt.transactionHash,
entityKey: data.entityKey,
expiresAt
};
}
//#endregion
//#region src/actions/wallet/patchEntity.ts
const logger = getLogger("actions:wallet:patch-entity");
async function patchEntity(client, schema, entityKey, changes, txParams) {
logger("patchEntity %s %o", entityKey, changes);
const { receipt } = await sendArkivTransaction(client, { patches: [{
...changes,
schema,
entityKey
}] }, txParams);
logger("Receipt from patchEntity %o", receipt);
return {
txHash: receipt.transactionHash,
entityKey
};
}
//#endregion
//#region src/clients/decorators/arkivWallet.ts
function walletArkivActions(client) {
return {
createEntity: (schema, values, options, txParams) => createEntity(client, schema, values, options, txParams),
patchEntity: (schema, entityKey, changes, txParams) => patchEntity(client, schema, entityKey, changes, txParams),
deleteEntity: (data, txParams) => deleteEntity(client, data, txParams),
extendEntity: (data, txParams) => extendEntity(client, data, txParams),
changeOwnership: (data, txParams) => changeOwnership(client, data, txParams),
executeBatch: (data, txParams) => executeBatch(client, data, txParams)
};
}
//#endregion
//#region src/clients/createWalletClient.ts
/**
* Creates a Wallet Client with a given [Transport](https://viem.sh/docs/clients/intro) configured
* for a [Chain](https://viem.sh/docs/chains/introduction) and an account.
*
* A Wallet Client signs and sends transactions, so it carries an `account` and exposes everything
* that mutates state — `createEntity`, `patchEntity`, `deleteEntity`, `extendEntity`,
* `changeOwnership` and `executeBatch` — alongside the viem actions those need, such as
* `getBlockNumber`, `readContract` and `waitForTransactionReceipt`. It does not have the Arkiv
* read actions (`getEntity`, `query`, `select`, `watchEntityEvents`, `predictEntityKeys`); read
* through a Public Client from `createPublicClient` for those.
*
* @param parameters - Configuration object for the wallet client (chain, transport, account, etc.)
* @returns An Arkiv Wallet Client. {@link WalletArkivClient}
*
* @example
* import { createWalletClient } from "@arkiv-network/sdk"
* import { rabdi } from "@arkiv-network/sdk/chains"
* import { http } from "viem"
* import { privateKeyToAccount } from "viem/accounts"
*
* const client = createWalletClient({
* chain: rabdi({ accessKey: "..." }),
* transport: http(),
* account: privateKeyToAccount("0x..."),
* })
*/
function createWalletClient(parameters) {
const { key: key$1 = "wallet", name = "Wallet Client" } = parameters;
return createClient({
...parameters,
key: key$1,
name
}).extend(publicActions).extend(walletActions).extend(walletArkivActions);
}
//#endregion
export { ConflictingMutationError, CountQueryBuilder, DECIMAL_SCALE, DECIMAL_UNIT, ENTITY_EVENTS_ABI, EmptyPatchError, Entity, EntityMutationError, EntityTooLargeError, ExpirationTime, I32_MAX, I32_MIN, INDEXED_FLAG, InvalidAttributeNameError, InvalidCreationFlagsError, InvalidExpiryError, InvalidSaltError, InvalidValueError, MAX_ATTRIBUTES, MAX_BYTES_LENGTH, MAX_ENTITY_BYTES, MAX_EXPIRES_AT, MAX_LIFETIME, MAX_NAME_BYTES, MAX_SALT, MAX_STRING_BYTES, MissingAccessKeyError, MissingValueError, NO_SALT, NoEntityFoundError, NoRandomSourceError, SCHEMA_ATTRIBUTE, SYSTEM_FIELDS, SchemaDefinitionError, SchemaMismatchError, SelectQueryBuilder, TooManyAttributesError, U256_MAX, U64_MAX, UnknownAttributeTypeError, UntypedValueError, addr, bool, bytes, createPublicClient, createWalletClient, dec, decFromUnits, decUnits, defineSchema, describeColumns, i32, isArkivValue, isColumn, isSchema, isValidAttributeName, key, lookupColumn, predictEntityKey, randomSalt, str, toValue, u256, u64, validateAttributeName };
//# sourceMappingURL=index.js.map