@arkiv-network/sdk
Version:
TypeScript client library for Arkiv's blockchains interactions
1,014 lines (1,004 loc) • 46.9 kB
JavaScript
import { checksumAddress, isAddress, isHex, pad, stringToBytes, toHex } from "viem";
//#region src/schema/errors.ts
/**
* A value was rejected by its tagged constructor — out of range, wrong length, bad checksum, too
* much precision, and so on.
*
* The message names the offending value and, where there is one, the type that *would* hold it, so
* the fix is in the error rather than in the docs.
*/
var InvalidValueError = class extends Error {
/** The type tag whose constructor rejected the value. */
tag;
/** The rejected input, as given. */
input;
constructor(tag, input, reason, hint) {
super(`Invalid ${tag} value ${describe(input)}: ${reason}.${hint ? ` ${hint}` : ""}`);
this.name = "InvalidValueError";
this.tag = tag;
this.input = input;
}
};
/**
* A bare JavaScript value could not be given a type. Raised when an attribute or predicate is
* handed something that is neither a tagged value nor one of the five bare forms
* (`boolean`, `number`, `bigint`, `string`, `Uint8Array`).
*/
var UntypedValueError = class extends Error {
input;
constructor(input) {
super(`Cannot infer an Arkiv type for ${describe(input)}. Attribute values must be a tagged value (i32, u64, u256, dec, str, addr, key, bytes, bool) or a bare boolean, number, bigint, string or Uint8Array.`);
this.name = "UntypedValueError";
this.input = input;
}
};
/**
* An attribute was given `undefined` or `null` as its value.
*
* There is no such thing as an attribute with no value: an attribute either exists with a type, or
* is not set. The fix is always to leave the name out rather than to write an empty value — or, to
* remove a column from an existing entity, to `unset` it in a patch — so this is called out
* separately from {@link UntypedValueError}: the remedy is different.
*
* `createEntity` and `patchEntity` already treat a column given as `undefined` as left out, so
* through them only `null` raises this.
*/
var MissingValueError = class extends Error {
/** The attribute whose value was missing, when raised from an attribute map. */
attributeName;
constructor(input, attributeName) {
const subject = attributeName ? `attribute "${attributeName}"` : "attribute value";
super(`The ${subject} is ${input === null ? "null" : "undefined"}. An attribute is either set with a value and a type or not set at all, so there is nothing to write here. ` + (attributeName ? `Leave "${attributeName}" out of the values (or a patch's "set") instead — e.g. build the object conditionally, or drop the key when the value is absent. To remove the column from an existing entity, list it in a patch's "unset".` : "Omit the attribute instead of giving it an empty value."));
this.name = "MissingValueError";
this.attributeName = attributeName;
}
};
/** An attribute name violated the on-chain name grammar. */
var InvalidAttributeNameError = class extends Error {
attributeName;
constructor(attributeName, reason) {
super(`Invalid attribute name "${attributeName}": ${reason}. Names must be 1-32 bytes, start with a letter, and contain only A-Z, a-z, 0-9, ".", "-" or "_". Names are case-sensitive.`);
this.name = "InvalidAttributeNameError";
this.attributeName = attributeName;
}
};
/**
* One patch tried to both write and remove the same attribute.
*/
var ConflictingMutationError = class extends Error {
attributeName;
constructor(attributeName) {
super(`The patch both sets and unsets "${attributeName}". A patch applies one mutation per attribute, so say which you mean: drop it from "unset" to write the value, or from "set" to remove the attribute.`);
this.name = "ConflictingMutationError";
this.attributeName = attributeName;
}
};
/**
* More attributes were supplied than an entity operation can carry — raised by `defineSchema` for a
* schema with more columns than one create could write.
*/
var TooManyAttributesError = class extends Error {
/**
* @param tagged - Whether `count` includes a named schema's tag, which the message then calls out:
* the tag is not among the declared columns, so the count would otherwise look off by one.
*/
constructor(count, max, tagged = false) {
super(`An entity may carry at most ${max} attributes, got ${count}.` + (tagged ? ` A named schema's tag counts as one of them, so it may declare at most ${max - 1} columns.` : ""));
this.name = "TooManyAttributesError";
}
};
/** The `bytes` values of one create or patch add up to more than `MAX_ENTITY_BYTES`. */
var EntityTooLargeError = class extends Error {
constructor(total, max) {
super(`The bytes columns of one create or patch may hold at most ${max} bytes in total, got ${total}. Split the data across entities, or store it elsewhere and keep a reference`);
this.name = "EntityTooLargeError";
}
};
/** A schema, or one of its columns, was defined in a way the SDK or the protocol cannot honour. */
var SchemaDefinitionError = class extends Error {
/** The column at fault, when there is one. */
columnName;
constructor(reason, columnName) {
super(`Invalid schema${columnName === void 0 ? "" : ` column "${columnName}"`}: ${reason}.`);
this.name = "SchemaDefinitionError";
this.columnName = columnName;
}
};
/**
* Data did not fit its schema: a write named a column the schema does not have, left out one it
* requires, or gave one a value of the wrong type — or an entity read back through a schema lacks a
* required column or holds one with a different type.
*/
var SchemaMismatchError = class extends Error {
/** The column at fault. */
columnName;
constructor(columnName, reason) {
super(`Column "${columnName}": ${reason}.`);
this.name = "SchemaMismatchError";
this.columnName = columnName;
}
};
/** The node sent back an attribute this SDK cannot decode. */
var UnknownAttributeTypeError = class extends Error {
/** The type as the node named it — a tag over JSON-RPC, a typeId on the wire. */
type;
constructor(type) {
super(`Unknown attribute type ${describe(type)}. The node is using a type this SDK version does not know about — upgrade @arkiv-network/sdk.`);
this.name = "UnknownAttributeTypeError";
this.type = type;
}
};
/** Renders an arbitrary input for an error message, without ever throwing. */
function describe(input) {
switch (typeof input) {
case "string": return JSON.stringify(input.length > 64 ? `${input.slice(0, 61)}...` : input);
case "bigint": return `${input}n`;
case "object": return input === null ? "null" : safeJson(input);
case "undefined": return "undefined";
default: return String(input);
}
}
function safeJson(input) {
try {
return JSON.stringify(input) ?? String(input);
} catch {
return Object.prototype.toString.call(input);
}
}
//#endregion
//#region src/schema/decimal.ts
/**
* Decimal places a `dec` value is scaled by. Fixed by the protocol, so any two decimals are
* directly comparable — and range-indexable — as plain integers.
*/
const DECIMAL_SCALE = 18;
/** 10 ** {@link DECIMAL_SCALE} — the scaling factor between a `dec` and its integer units. */
const DECIMAL_UNIT = 10n ** BigInt(DECIMAL_SCALE);
/** The widest scaled value an `int256` holds. */
const INT256_MAX = 2n ** 255n - 1n;
/** The narrowest scaled value an `int256` holds. */
const INT256_MIN = -(2n ** 255n);
/**
* A decimal literal: an optional sign, at least one integer digit, and an optional fraction of at
* most {@link DECIMAL_SCALE} digits. Exponents are not part of the grammar — `1e18` is a parse
* error rather than a silent reinterpretation.
*/
const DECIMAL_RE = /^([+-]?)(\d+)(?:\.(\d+))?$/;
/**
* Parses a decimal literal into its scaled integer units.
*
* Excess fractional precision is an **error, never a rounding**: the protocol stores exactly 18
* decimal places, and silently dropping the 19th digit would make a stored value differ from the
* one that was written.
*
* @param input - The decimal literal, e.g. `"3.5"` or `"-0.000001"`.
* @returns The value scaled by 10 ** 18.
* @throws {InvalidValueError} If the literal is malformed, too precise, or out of `int256` range.
*/
function decimalToUnits(input) {
const match = DECIMAL_RE.exec(input);
if (!match) throw new InvalidValueError("dec", input, "not a decimal literal", "Write it as sign, digits, and an optional \".\" with up to 18 fractional digits — e.g. dec(\"3.5\") or dec(\"-0.25\"). Exponents such as \"1e18\" are not accepted.");
const [, sign, whole, fraction = ""] = match;
if (fraction.length > DECIMAL_SCALE) throw new InvalidValueError("dec", input, `${fraction.length} fractional digits exceeds the ${DECIMAL_SCALE} the protocol stores`, "Round it yourself before storing — a dec is never rounded for you, so that what you write is exactly what is stored.");
const digits = BigInt(whole + fraction.padEnd(DECIMAL_SCALE, "0"));
const units = sign === "-" ? -digits : digits;
if (units > INT256_MAX || units < INT256_MIN) throw new InvalidValueError("dec", input, "exceeds the int256 range a dec is stored in");
return units;
}
/**
* Renders scaled integer units back as a canonical decimal string: no leading zeros, no trailing
* fractional zeros, no `-0`. Parsing the result yields the same units, so this is the inverse of
* {@link decimalToUnits} on every value it accepts.
*
* @param units - The value scaled by 10 ** 18.
* @returns The canonical decimal string, e.g. `"3.5"`.
* @throws {InvalidValueError} If the units are outside the `int256` range.
*/
function unitsToDecimal(units) {
if (units > INT256_MAX || units < INT256_MIN) throw new InvalidValueError("dec", units, "exceeds the int256 range a dec is stored in");
const negative = units < 0n;
const magnitude = negative ? -units : units;
const whole = magnitude / DECIMAL_UNIT;
const trimmed = (magnitude % DECIMAL_UNIT).toString().padStart(DECIMAL_SCALE, "0").replace(/0+$/, "");
const sign = negative ? "-" : "";
return trimmed ? `${sign}${whole}.${trimmed}` : `${sign}${whole}`;
}
//#endregion
//#region src/schema/types.ts
/**
* The protocol's attribute type vocabulary, mapping each short **tag** (the name used by the
* client, the query language and this SDK) to its **typeId** (the byte carried on the wire and in
* the entity record).
*
* The typeIds are consensus-critical: they are written into entity records and mixed into index
* keys, so this table must never drift from the node's `AttributeType`.
*
* | tag | typeId | ABI wire type | when indexed |
* | ------- | ------ | -------------------- | ------------------------- |
* | `bool` | 1 | `bool` | equality + range |
* | `i32` | 2 | `int32` | equality + range |
* | `u64` | 3 | `uint64` | equality + range |
* | `u256` | 4 | `uint256` | equality + range |
* | `dec` | 5 | `int256` (18 dp) | equality + range |
* | — | 6 | `bytes32` | reserved |
* | `bytes` | 7 | `bytes` (<= 128 KiB) | never indexed |
* | `str` | 8 | `string` (<= 128 B) | equality + range + prefix |
* | `addr` | 9 | `address` | equality + range |
* | `key` | 10 | `bytes32` | equality + range |
*
* typeId 6 is the node's `bytes32` type, which this SDK does not expose: it is reserved here, so
* `TYPE_IDS` leaves it out, and a value of that type is read as a `key`.
*
* On the wire the typeId shares its byte with the indexed flag — see {@link INDEXED_FLAG}.
*/
const TYPE_IDS = {
bool: 1,
i32: 2,
u64: 3,
u256: 4,
dec: 5,
bytes: 7,
str: 8,
addr: 9,
key: 10
};
/**
* Bit 7 of the `typeId` byte: set when the node should index the attribute. The low seven bits are
* the type itself, so `str` travels as `8` unindexed and `0x88` indexed.
*/
const INDEXED_FLAG = 128;
/** Whether attributes of this type may be indexed — everything except `bytes`. */
function isIndexable(tag) {
return tag !== "bytes";
}
/**
* Builds a validated value. Module-internal: the constructors in `values.ts` call this **after**
* validating, which is what makes the {@link validated} brand meaningful.
*/
function makeValue(type, value) {
return {
type,
value
};
}
/**
* Whether `input` has the *shape* of an {@link ArkivValue} — a known type tag and a value.
*/
function isArkivValue(input) {
return typeof input === "object" && input !== null && "type" in input && "value" in input && typeof input.type === "string" && Object.hasOwn(TYPE_IDS, input.type);
}
//#endregion
//#region src/schema/column.ts
/**
* Marks a column. A registry symbol rather than `instanceof`, so a column made by one copy of the
* SDK — its ESM build, say — is still recognised by another, such as the CJS build a dependency
* loaded.
*/
const COLUMN_BRAND = Symbol.for("@arkiv-network/sdk/column");
var ColumnDefinition = class ColumnDefinition {
type;
isIndexed;
isOptional;
defaultValue;
#coerce;
constructor(type, isIndexed, isOptional, defaultValue, coerce) {
this.type = type;
this.isIndexed = isIndexed;
this.isOptional = isOptional;
this.defaultValue = defaultValue;
this.#coerce = coerce;
Object.defineProperty(this, COLUMN_BRAND, { value: true });
Object.freeze(this);
}
indexed() {
if (this.type === "bytes") throw new SchemaDefinitionError("a bytes column cannot be indexed. Bytes are stored, never indexed, so they can be selected but not filtered on — index a hash of them, as a hex str, instead");
return this.#with({ isIndexed: true });
}
optional() {
return this.#with({ isOptional: true });
}
default(...args) {
if (args.length === 0) throw new SchemaDefinitionError(`.default() needs a value — e.g. ${this.type}().default(...). Leave .default() off for a column the create must always provide`);
return this.#with({ defaultValue: this.#coerce(this.type, args[0]) });
}
#with(changes) {
return new ColumnDefinition(this.type, changes.isIndexed ?? this.isIndexed, changes.isOptional ?? this.isOptional, changes.defaultValue ?? this.defaultValue, this.#coerce);
}
};
/**
* A fresh column of type `tag`: unindexed, required, no default. Internal seam for the type
* constructors, which return one when called with no arguments.
*/
function createColumn(tag, coerce) {
return new ColumnDefinition(tag, false, false, void 0, coerce);
}
/** Whether `input` is a column, as opposed to a value or anything else. */
function isColumn(input) {
return typeof input === "object" && input !== null && COLUMN_BRAND in input;
}
//#endregion
//#region src/schema/values.ts
/** The widest `i32`. */
const I32_MAX = 2147483647;
/** The narrowest `i32`. */
const I32_MIN = -2147483648;
/** The widest `u64`. */
const U64_MAX = 2n ** 64n - 1n;
/** The widest `u256`. */
const U256_MAX = 2n ** 256n - 1n;
/**
* The longest `str`, in UTF-8 bytes
*/
const MAX_STRING_BYTES = 128;
/**
* The longest `bytes` value, in bytes. The cap applies to each `bytes` attribute separately, but
* all the `bytes` values of one create or patch also share the `MAX_ENTITY_BYTES` budget.
*/
const MAX_BYTES_LENGTH = 128 * 1024;
/**
* Finds the first lone UTF-16 surrogate — a string the node would reject, since it has no UTF-8
* encoding. `TextEncoder` would quietly replace it with U+FFFD instead.
*/
function findLoneSurrogate(value) {
for (let index = 0; index < value.length; index++) {
const code = value.charCodeAt(index);
if (code < 55296 || code > 57343) continue;
const next = value.charCodeAt(index + 1);
if (code <= 56319 && next >= 56320 && next <= 57343) {
index++;
continue;
}
return index;
}
}
function bool(...args) {
if (args.length === 0) return createColumn("bool", toTypedValue);
const [value] = args;
if (typeof value !== "boolean") throw new InvalidValueError("bool", value, "not a boolean");
return makeValue("bool", value);
}
function i32(...args) {
if (args.length === 0) return createColumn("i32", toTypedValue);
const [value] = args;
if (typeof value === "bigint") {
if (value > BigInt(I32_MAX) || value < BigInt(I32_MIN)) throw new InvalidValueError("i32", value, outOfI32Range(value >= 0n));
return makeValue("i32", Number(value));
}
if (typeof value !== "number" || !Number.isFinite(value)) throw new InvalidValueError("i32", value, "not a number");
if (!Number.isInteger(value)) throw new InvalidValueError("i32", value, "not an integer", `Use dec(${JSON.stringify(String(value))}) for a fixed-point decimal.`);
if (value > I32_MAX || value < I32_MIN) throw new InvalidValueError("i32", value, outOfI32Range(value >= 0));
return makeValue("i32", value);
}
function outOfI32Range(nonNegative) {
const range = `outside the i32 range [${I32_MIN}, ${I32_MAX}]`;
return nonNegative ? `${range} — use u64(...) for large counters, timestamps and block heights (Date.now() does not fit an i32), or u256(...) beyond 2**64 - 1` : `${range} — use dec(...) for values that need more range than an i32`;
}
function u256(...args) {
if (args.length === 0) return createColumn("u256", toTypedValue);
return makeValue("u256", toUnsigned("u256", args[0], U256_MAX));
}
function u64(...args) {
if (args.length === 0) return createColumn("u64", toTypedValue);
return makeValue("u64", toUnsigned("u64", args[0], U64_MAX));
}
/**
* Parses and range-checks an unsigned integer for `tag`, whose widest value is `max`.
*
* Shared by {@link u64} and {@link u256} so the two differ in exactly one thing — their width —
* rather than in how they read a string or how they explain a failure.
*/
function toUnsigned(tag, value, max) {
const parsed = toUnsignedBigint(tag, value);
if (parsed < 0n) throw new InvalidValueError(tag, value, `negative — ${tag} is unsigned`, "Use i32(...) for small signed integers, or dec(...) for a signed fixed-point value.");
if (parsed > max) throw new InvalidValueError(tag, value, `wider than ${tag === "u64" ? 64 : 256} bits`, tag === "u64" ? "Use u256(...) for values beyond 2**64 - 1." : void 0);
return parsed;
}
function toUnsignedBigint(tag, value) {
if (typeof value === "bigint") return value;
if (typeof value === "number") {
if (!Number.isInteger(value)) throw new InvalidValueError(tag, value, "not an integer", `Use dec(${JSON.stringify(String(value))}) for a fixed-point decimal.`);
if (!Number.isSafeInteger(value)) throw new InvalidValueError(tag, value, "beyond the safe integer range, so it may already have lost precision", "Pass it as a bigint literal (e.g. 12345678901234567890n) or a string instead.");
return BigInt(value);
}
if (typeof value === "string") {
if (!/^(0x[0-9a-fA-F]+|\d+)$/.test(value)) throw new InvalidValueError(tag, value, "not a decimal or 0x-hex integer");
return BigInt(value);
}
throw new InvalidValueError(tag, value, "not a bigint, number or string");
}
function dec(...args) {
if (args.length === 0) return createColumn("dec", toTypedValue);
const [value] = args;
if (typeof value === "string") return makeValue("dec", unitsToDecimal(decimalToUnits(value)));
if (typeof value === "bigint") return decFromUnits(value * DECIMAL_UNIT);
if (typeof value !== "number" || !Number.isFinite(value)) throw new InvalidValueError("dec", value, "not a number");
if (!Number.isInteger(value)) throw new InvalidValueError("dec", value, "a fractional number cannot be represented exactly in binary floating point", `Pass it as a string instead: dec(${JSON.stringify(String(value))}).`);
return decFromUnits(BigInt(value) * DECIMAL_UNIT);
}
/**
* The exact scaled integer behind a `dec` — the value multiplied by 10 ** 18, which is what the
* protocol stores and compares. Use it when you need exact arithmetic; JavaScript has no native
* decimal type, so the SDK carries the canonical string and leaves the maths to you.
*
* @example
* decUnits(dec("3.5")) // 3500000000000000000n
*/
function decUnits(value) {
return decimalToUnits(value.value);
}
/**
* A `dec` built from its scaled integer units — the inverse of {@link decUnits}.
*
* @example
* decFromUnits(3_500_000_000_000_000_000n) // dec("3.5")
*/
function decFromUnits(units) {
return makeValue("dec", unitsToDecimal(units));
}
function str(...args) {
if (args.length === 0) return createColumn("str", toTypedValue);
const [value] = args;
if (typeof value !== "string") throw new InvalidValueError("str", value, "not a string");
const surrogate = findLoneSurrogate(value);
if (surrogate !== void 0) throw new InvalidValueError("str", value, `contains a lone surrogate at index ${surrogate}, so it is not valid UTF-8`);
const bytes$1 = stringToBytes(value).length;
if (bytes$1 > MAX_STRING_BYTES) throw new InvalidValueError("str", value, `${bytes$1} UTF-8 bytes exceeds the ${MAX_STRING_BYTES}-byte limit on a str`, "Store the long form in a bytes column and keep a short, queryable key here.");
return makeValue("str", value);
}
function addr(...args) {
if (args.length === 0) return createColumn("addr", toTypedValue);
const [value] = args;
if (typeof value !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(value)) throw new InvalidValueError("addr", value, "not a 20-byte 0x-prefixed hex address");
const body = value.slice(2);
if (body !== body.toLowerCase() && body !== body.toUpperCase() && !isAddress(value, { strict: true })) throw new InvalidValueError("addr", value, "fails its EIP-55 checksum, so a digit is probably mistyped", `If those hex digits are right, the correctly checksummed form is ${checksumAddress(value)}.`);
return makeValue("addr", checksumAddress(value));
}
function key(...args) {
if (args.length === 0) return createColumn("key", toTypedValue);
return makeValue("key", asFixedHex("key", args[0], 32));
}
function bytes(...args) {
if (args.length === 0) return createColumn("bytes", toTypedValue);
const [value] = args;
const hex = asHex("bytes", value);
const length = (hex.length - 2) / 2;
if (length > MAX_BYTES_LENGTH) throw new InvalidValueError("bytes", `<${length} bytes>`, `${length} bytes exceeds the ${MAX_BYTES_LENGTH}-byte limit on a bytes value`, "Split it across several entities, or store the bulk elsewhere and keep a reference.");
return makeValue("bytes", hex);
}
/** Normalizes hex-or-bytes input of a known width into lowercase `0x` hex. */
function asFixedHex(tag, value, size$1) {
if (value instanceof Uint8Array) {
if (value.length !== size$1) throw new InvalidValueError(tag, `<${value.length} bytes>`, `not exactly ${size$1} bytes`);
return toHex(value);
}
const hex = asHex(tag, value);
if (hex.length !== 2 + size$1 * 2) throw new InvalidValueError(tag, value, `${(hex.length - 2) / 2} bytes, not exactly ${size$1} bytes`);
return hex;
}
/** Normalizes hex-or-bytes input into lowercase `0x` hex of a whole number of bytes. */
function asHex(tag, value) {
if (value instanceof Uint8Array) return toHex(value);
if (typeof value !== "string" || !isHex(value, { strict: true })) throw new InvalidValueError(tag, value, "not 0x-prefixed hex");
if (value.length % 2 !== 0) throw new InvalidValueError(tag, value, "an odd number of hex digits, so not whole bytes");
return value.toLowerCase();
}
/**
* Resolves a {@link ValueInput} to a typed value, applying the bare-form defaults above. Tagged
* values pass through untouched.
*
* @param input - The value to resolve.
* @param attributeName - The attribute this value belongs to, used to make errors name it.
*
* @throws {MissingValueError} If the input is `undefined` or `null` — an attribute is either set
* with a value and a type, or not set at all.
* @throws {UntypedValueError} If the input is not a tagged value or a bare boolean, number, bigint,
* string or `Uint8Array`.
* @throws {InvalidValueError} If a bare value does not fit the type it defaults to.
*
* @example
* toValue(10) // i32(10)
* toValue(10n) // u256(10n)
* toValue("hello") // str("hello")
* toValue(dec("3.5")) // unchanged
*/
function toValue(input, attributeName) {
if (input === void 0 || input === null) throw new MissingValueError(input, attributeName);
switch (typeof input) {
case "boolean": return bool(input);
case "number": return i32(input);
case "bigint": return u256(input);
case "string": return str(input);
default: {
if (input instanceof Uint8Array) return bytes(input);
if (isArkivValue(input)) return revalidate(input);
const candidate = input;
if (isColumn(candidate)) throw columnGivenAsValue(candidate);
throw new UntypedValueError(input);
}
}
}
/**
* Resolves `input` to a value of type `tag` — a tagged value of that type, or whatever `tag`'s
* constructor accepts. What a column's `.default(...)` runs its argument through.
*
* @throws {InvalidValueError} If the input does not make a valid value of type `tag`.
*/
function toTypedValue(tag, input) {
if (isArkivValue(input)) {
const value = revalidate(input);
if (value.type !== tag) throw new InvalidValueError(tag, input, `a ${value.type} value, not a ${tag}`);
return value;
}
if (isColumn(input)) throw columnGivenAsValue(input);
const construct = CONSTRUCTORS[tag];
return construct(input);
}
function columnGivenAsValue(column) {
return new InvalidValueError(column.type, `<${column.type} column>`, "a column, not a value", `Pass the value itself, e.g. ${column.type}(...) with an argument — ${column.type}() with none declares a schema column.`);
}
/** Each tag's constructor, called with a value. */
const CONSTRUCTORS = {
bool,
i32,
u64,
u256,
dec,
bytes,
str,
addr,
key
};
/**
* Re-runs a tagged value through its own constructor.
*
* {@link isArkivValue} can only check shape — the `validated` brand is a type-level construct with
* no runtime existence, so a hand-written `{ type: "u256", value: "1000" }` (from `JSON.parse`, a
* config file, an `as any` boundary) is indistinguishable from a real `u256(1000n)`. Passing it
* through unchecked was silent corruption rather than an error: the ABI encoder would take the
* *string* "1000" and write its UTF-8 bytes into the word, storing 2.27e76 on chain.
*
* Running the constructor again is what makes the brand's promise — "every value crossing the wire
* has been range-, length- and format-checked" — true at runtime. For a value that really did come
* from a constructor it is a no-op that returns an equal value.
*/
function revalidate(value) {
switch (value.type) {
case "bool": return bool(value.value);
case "i32": return i32(value.value);
case "u64": return u64(value.value);
case "u256": return u256(value.value);
case "dec": return dec(value.value);
case "bytes": return bytes(value.value);
case "str": return str(value.value);
case "addr": return addr(value.value);
case "key": return key(value.value);
}
}
//#endregion
//#region src/schema/codec.ts
function strFromChain(value) {
return makeValue("str", value);
}
/**
* The `typeId` marking a tombstone — an attribute being unset. It carries a zero-length value and
* is legal only in a patch's mutation list, never in a create.
*/
const TOMBSTONE_TYPE_ID = 0;
/**
* The node's `bytes32` type, which this SDK does not expose. Its values are 32 bytes, as a `key`'s
* are, so they are read as `key`s.
*/
const BYTES32_TYPE_ID = 6;
const BYTES32_TAG = "bytes32";
/**
* Encodes a typed value into the `bytes` the ABI carries for it.
*
* This and {@link decodeValueBytes} are the only place the value/wire mapping is written down.
*
* - Word types (`bool`, `i32`, `u64`, `u256`, `dec`, `addr`, `key`) become exactly 32
* bytes, in the encoding Solidity would use — right-aligned, and sign-extended for `i32`.
* - `str` becomes its raw UTF-8 bytes, and `bytes` its raw bytes: variable length, no padding.
*/
function encodeValueBytes(value) {
switch (value.type) {
case "bool": return pad(value.value ? "0x01" : "0x00", { size: 32 });
case "i32": return toHex(BigInt.asUintN(256, BigInt(value.value)), { size: 32 });
case "u64":
case "u256": return toHex(value.value, { size: 32 });
case "dec": return toHex(BigInt.asUintN(256, decUnits(value)), { size: 32 });
case "key": return value.value;
case "addr": return pad(value.value.toLowerCase(), { size: 32 });
case "str": return toHex(stringToBytes(value.value));
case "bytes": return value.value;
}
}
/**
* Encodes a named, typed attribute into its ABI form.
*
* @param name - The attribute name. Assumed already validated.
* @param indexed - Whether the node should index it. Never true for `bytes`, which the engine
* rejects with the flag set.
* @throws {InvalidValueError} If asked to index a `bytes` value.
*/
function encodeAbiAttribute(name, value, indexed) {
if (indexed && value.type === "bytes") throw new InvalidValueError("bytes", `<${name}>`, "bytes are stored, never indexed");
return {
name: toHex(name, { size: 32 }),
typeId: TYPE_IDS[value.type] | (indexed ? INDEXED_FLAG : 0),
value: encodeValueBytes(value)
};
}
/**
* Splits a wire `typeId` into its type and indexed flag.
*
* The node's `bytes32` (typeId 6) decodes as `key`.
*
* @throws {UnknownAttributeTypeError} If the low seven bits name no known type.
*/
function decodeTypeId(typeId) {
const base = typeId & ~INDEXED_FLAG;
const tag = base === BYTES32_TYPE_ID ? "key" : Object.keys(TYPE_IDS).find((candidate) => TYPE_IDS[candidate] === base);
if (tag === void 0) throw new UnknownAttributeTypeError(typeId);
return {
tag,
indexed: (typeId & INDEXED_FLAG) !== 0
};
}
/** Encodes a tombstone — the mutation that unsets `name`. Legal only in a patch. */
function encodeTombstone(name) {
return {
name: toHex(name, { size: 32 }),
typeId: TOMBSTONE_TYPE_ID,
value: "0x"
};
}
/**
* Decodes an attribute value as it comes back over JSON-RPC.
*
* The JSON encoding follows the value's **declared type**, never its magnitude: `bool` is a JSON
* boolean, `i32` a JSON number, `u64`/`u256` a `0x` QUANTITY, `dec` a decimal string, `str` a
* string, and
* the byte-shaped types `0x` DATA. Decimal strings are also accepted for the integer types, so this
* keeps working against a node that renders them that way.
*
* @param type - The type tag the response declared, e.g. `"u256"`.
* @param value - The JSON value.
* @throws {UnknownAttributeTypeError} If the tag names no known type.
* @throws {InvalidValueError} If the value does not parse as that type.
*/
function decodeRpcValue(type, value) {
switch (asTypeTag(type)) {
case "bool": return bool(asBoolean(value));
case "i32": return i32(Number(asIntegerish("i32", value)));
case "u64": return u64(asIntegerish("u64", value));
case "u256": return u256(asIntegerish("u256", value));
case "dec": return dec(asString("dec", value));
case "key": return key(asString("key", value));
case "addr": return addr(asString("addr", value));
case "str": return strFromChain(asString("str", value));
case "bytes": return makeValue("bytes", asString("bytes", value).toLowerCase());
}
}
/**
* Narrows a type tag as the node spelled it. The node's `bytes32` narrows to `key`.
*
* @throws {UnknownAttributeTypeError} If it names no type this SDK knows.
*/
function asTypeTag(type) {
if (typeof type !== "string" || !Object.hasOwn(TYPE_IDS, type)) {
if (type === BYTES32_TAG) return "key";
throw new UnknownAttributeTypeError(type);
}
return type;
}
function asBoolean(value) {
if (typeof value === "boolean") return value;
if (value === "true" || value === "false") return value === "true";
throw new InvalidValueError("bool", value, "not a boolean");
}
function asString(tag, value) {
if (typeof value !== "string") throw new InvalidValueError(tag, value, `expected a string, got ${typeof value}`);
return value;
}
/** A `0x` QUANTITY, a JSON number, or a decimal string — all of which name one integer. */
function asIntegerish(tag, value) {
if (typeof value === "number") {
if (!Number.isInteger(value)) throw new InvalidValueError(tag, value, "not an integer");
return BigInt(value);
}
if (typeof value === "bigint") return value;
if (typeof value === "string") {
if (/^0x[0-9a-fA-F]+$/.test(value)) return BigInt(value);
if (/^-?\d+$/.test(value)) return BigInt(value);
}
throw new InvalidValueError(tag, value, "not an integer");
}
//#endregion
//#region src/schema/names.ts
/** The longest attribute name, in bytes — the width of the on-chain `Ident32` name cell. */
const MAX_NAME_BYTES = 32;
/**
* The attribute-name grammar (`Ident32`): a leading letter, then letters, digits, `.`, `-` or `_`.
*
* Names are **case-sensitive** — `Level` and `level` are two different attributes.
*
* The charset is ASCII-only, so a name's length in characters is also its length in bytes.
*/
const NAME_RE = /^[A-Za-z][A-Za-z0-9._-]*$/;
/**
* Words the query language reserves. They are rejected as attribute names because an attribute
* named `not` or `str` could be written but never queried — the parser would read the name as an
* operator or a type tag.
*
* Matched **case-insensitively**: query keywords are case-insensitive, so `NOT` and `not` are the
* same operator, and the type tags are held to the same rule so that what is reserved does not
* depend on how you happen to capitalise it.
*/
const RESERVED = new Set([
"and",
"or",
"not",
"true",
"false",
"between",
"startswith",
"exists",
"typeof",
"order",
"by",
"asc",
"desc",
"bool",
"i32",
"u64",
"u256",
"dec",
"bytes",
"str",
"addr",
"key"
]);
/**
* Validates an attribute name against the engine's `Ident32` grammar and the query language's
* reserved words.
*
* @param name - The attribute name.
* @throws {InvalidAttributeNameError} If the name is empty, starts with `$` (reserved for system
* attributes), too long, uses characters outside the grammar, contains `--` (which opens a query
* comment), or collides with a reserved word.
*/
function validateAttributeName(name) {
if (typeof name !== "string" || name.length === 0) throw new InvalidAttributeNameError(String(name), "the name is empty");
if (name.startsWith("$")) throw new InvalidAttributeNameError(name, "\"$\" is reserved for system attributes, which the engine sets and you cannot write");
if (!NAME_RE.test(name)) {
const chars = [...name];
const at = chars.findIndex((char, index) => !(index === 0 ? /[A-Za-z]/ : /[A-Za-z0-9._-]/).test(char));
throw new InvalidAttributeNameError(name, at === 0 ? `it starts with "${chars[0]}" rather than a letter` : `it contains "${chars[at]}" at position ${at}`);
}
if (name.length > MAX_NAME_BYTES) throw new InvalidAttributeNameError(name, `the name is ${name.length} bytes long`);
if (name.includes("--")) throw new InvalidAttributeNameError(name, "\"--\" opens a comment in the query language, so everything after it in a query would be discarded — an attribute with this name could be written but never safely queried");
if (RESERVED.has(name.toLowerCase())) throw new InvalidAttributeNameError(name, `"${name}" is reserved by the query language, so an attribute with this name could never be queried`);
}
/** Whether `name` is a valid attribute name — the non-throwing form of {@link validateAttributeName}. */
function isValidAttributeName(name) {
try {
validateAttributeName(name);
return true;
} catch {
return false;
}
}
//#endregion
//#region src/schema/schema.ts
/**
* The most attributes one entity operation may carry, and so the most columns a schema may have —
* one fewer for a named schema, whose tag is one more attribute on every create.
*/
const MAX_ATTRIBUTES = 32;
/**
* The attribute a named schema tags its entities with: an indexed `str` holding the schema's name.
* Creates write it, and `.from(schema)` filters on it, so a query through a schema only ever sees
* entities written through it.
*/
const SCHEMA_ATTRIBUTE = "arkiv.schema";
/**
* Marks a schema. A registry symbol rather than `instanceof`, so a schema defined through one copy
* of the SDK — its CJS build, say — is still recognised by another, such as the ESM build an app
* loaded.
*/
const SCHEMA_BRAND = Symbol.for("@arkiv-network/sdk/schema");
/**
* A schema: the named, typed attributes an entity of one kind carries, and which of them the node
* indexes. Built with {@link defineSchema}.
*
* 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 the SDK's writes and reads, and checks them at runtime.
*
* A named schema also tags every entity it creates with its name, in the indexed
* {@link SCHEMA_ATTRIBUTE} attribute, so that a query through it sees only its own entities.
*
* @typeParam C - The columns, keyed by attribute name.
*/
var Schema = class {
/** The name entities are tagged with, or `undefined` for an untagged schema. */
name;
/** The columns, keyed by attribute name, in the order they were declared. */
columns;
/** The tag every create writes and every query filters on, if the schema is named. */
tag;
/** Use {@link defineSchema}. */
constructor(name, columns) {
this.tag = name === void 0 ? void 0 : validateName(name);
validateColumns(columns, name !== void 0);
this.name = name;
this.columns = Object.freeze({ ...columns });
Object.defineProperty(this, SCHEMA_BRAND, { value: true });
Object.freeze(this);
}
};
function defineSchema(...args) {
return args.length === 2 ? new Schema(args[0], args[1]) : new Schema(void 0, args[0]);
}
function validateName(name) {
if (typeof name !== "string" || name.length === 0) throw new SchemaDefinitionError("a schema name must be a non-empty string — leave it out for an untagged schema");
try {
return str(name);
} catch (error) {
if (!(error instanceof InvalidValueError)) throw error;
throw new SchemaDefinitionError(`the name is not a valid str tag. ${error.message}`);
}
}
function validateColumns(columns, tagged) {
if (typeof columns !== "object" || columns === null || Array.isArray(columns)) throw new SchemaDefinitionError("defineSchema takes an object of columns keyed by name");
const entries = Object.entries(columns);
const attributes = entries.length + (tagged ? 1 : 0);
if (attributes > MAX_ATTRIBUTES) throw new TooManyAttributesError(attributes, MAX_ATTRIBUTES, tagged);
for (const [name, column] of entries) {
validateAttributeName(name);
if (name === SCHEMA_ATTRIBUTE) throw new SchemaDefinitionError(`"${name}" is the attribute a named schema tags its entities with, so a column cannot use it. Pick another name`, name);
if (!isColumn(column)) throw notAColumn(name, column);
}
}
/** Whether `input` is a schema built with {@link defineSchema}. */
function isSchema(input) {
return typeof input === "object" && input !== null && SCHEMA_BRAND in input;
}
/**
* The column `name` of `schema`, or `undefined` if it has none. Only the schema's own columns
* count, so an inherited key such as `toString` or `__proto__` is not mistaken for one.
*/
function lookupColumn(schema, name) {
return Object.hasOwn(schema.columns, name) ? schema.columns[name] : void 0;
}
/** The schema's column names, for an error message that has to say what would have worked. */
function describeColumns(schema) {
return Object.keys(schema.columns).join(", ") || "(none)";
}
function notAColumn(name, input) {
if (isArkivValue(input)) return new SchemaDefinitionError(`got a ${input.type} value where a column belongs. Did you mean ${input.type}().default(${defaultArgument(input)})? Called with a value, ${input.type}(...) makes a value; called with none, ${input.type}() declares a column`, name);
return new SchemaDefinitionError("not a column. Declare it with a type constructor called with no arguments, e.g. str() or dec().indexed()", name);
}
/** A value spelled as the argument its constructor would take, for an error message. */
function defaultArgument(value) {
switch (value.type) {
case "bool":
case "i32": return String(value.value);
case "u64":
case "u256": return `${value.value}n`;
default: return JSON.stringify(value.value);
}
}
//#endregion
//#region src/schema/attributes.ts
/**
* The most data the `bytes` columns of one create or patch may hold together, in bytes. Each
* `bytes` value is also capped on its own, at `MAX_BYTES_LENGTH`.
*
* Provisional: the protocol has not settled a per-entity cap yet, so this keeps the budget the
* single payload had — and an operation much larger than it would not fit in a transaction the
* node accepts anyway.
*/
const MAX_ENTITY_BYTES = 128 * 1024;
/**
* Encodes a create's values into the ABI attribute array it carries: every column the
* values name, plus the default of every defaulted column they leave out, each flagged indexed or
* not as its column says — and, for a named schema, the indexed tag that marks the entity as its.
*
* @throws {SchemaDefinitionError} If `schema` is not a schema built with `defineSchema`.
* @throws {TypeError} If `values` is not an object.
* @throws {SchemaMismatchError} If a value names a column the schema lacks, a required column is
* left out, or a value's type is not its column's.
* @throws {MissingValueError} If a value is `null`.
* @throws {UntypedValueError} If a value is neither a tagged value nor a bare form.
* @throws {InvalidValueError} If a value does not fit its type.
* @throws {EntityTooLargeError} If the `bytes` values add up to more than {@link MAX_ENTITY_BYTES}.
*/
function encodeCreate(schema, values) {
assertSchema(schema);
for (const name of keysOf(values, "create")) columnFor(schema, name, "create");
const attributes = [];
if (schema.tag !== void 0) attributes.push(encodeAbiAttribute(SCHEMA_ATTRIBUTE, schema.tag, true));
for (const [name, column] of Object.entries(schema.columns)) {
const input = Object.hasOwn(values, name) ? values[name] : void 0;
if (input === void 0) {
if (column.defaultValue !== void 0) attributes.push(encodeAbiAttribute(name, column.defaultValue, column.isIndexed));
else if (!column.isOptional) throw new SchemaMismatchError(name, `the create leaves out a required ${column.type} column. Give it a value, or declare it ${column.type}().optional() or with a .default(...)`);
continue;
}
attributes.push(encodeColumn(name, column, input));
}
assertBytesFit(attributes);
return attributes.sort(byName);
}
/**
* Encodes a patch into the ABI attribute array it carries: a write for each column in `set`, with
* the indexed flag its column declares, and a tombstone for each name in `unset`. A column `set`
* to `undefined` is left out, as it is from a create.
*
* Writing a column with a different indexed flag than the entity holds flips its indexing, so a
* schema that marks a column `.indexed()` indexes it from the next patch that writes it.
*
* @throws {SchemaDefinitionError} If `schema` is not a schema built with `defineSchema`.
* @throws {SchemaMismatchError} If a name is not a column, a value's type is not its column's, or
* `unset` names a required column.
* @throws {ConflictingMutationError} If a name appears in both `set` and `unset`.
* @throws {MissingValueError} If a value in `set` is `null`.
* @throws {UntypedValueError} If a value in `set` is neither a tagged value nor a bare form.
* @throws {InvalidValueError} If a value does not fit its type.
* @throws {EntityTooLargeError} If the `bytes` values in `set` add up to more than
* {@link MAX_ENTITY_BYTES}.
* @throws {TypeError} If `set` is not an object, or `unset` is not an array.
*/
function encodePatch(schema, { set = {}, unset = [] }) {
assertSchema(schema);
const mutations = [];
for (const name of keysOf(set, "patch")) {
const column = columnFor(schema, name, "patch");
const input = set[name];
if (input !== void 0) mutations.push(encodeColumn(name, column, input));
}
if (!Array.isArray(unset)) throw new TypeError(`"unset" must be an array of column names`);
for (const name of new Set(unset)) {
const column = columnFor(schema, name, "patch");
if (!column.isOptional) throw new SchemaMismatchError(name, `a required column, so unsetting it would leave the entity without a value its schema requires. Declare it ${column.type}().optional() to allow that`);
if (Object.hasOwn(set, name) && set[name] !== void 0) throw new ConflictingMutationError(name);
mutations.push(encodeTombstone(name));
}
assertBytesFit(mutations);
return mutations.sort(byName);
}
function assertSchema(schema) {
if (!isSchema(schema)) throw new SchemaDefinitionError("expected a schema built with defineSchema(...) — writes are typed by the schema they name");
}
function keysOf(values, operation) {
if (typeof values !== "object" || values === null) throw new TypeError(`The ${operation} values must be an object keyed by column name`);
return Object.keys(values);
}
function columnFor(schema, name, operation) {
const column = lookupColumn(schema, name);
if (column === void 0) throw new SchemaMismatchError(name, `the ${operation} names a column this schema does not have. Its columns are ` + describeColumns(schema));
return column;
}
/** One column's value, resolved, checked against the column's type, and encoded. */
function encodeColumn(name, column, input) {
const value = toValue(input, name);
if (value.type !== column.type) throw new SchemaMismatchError(name, `a ${column.type} column, but the value is a ${value.type}. Write ${column.type}(...) around the value`);
return encodeAbiAttribute(name, value, column.isIndexed);
}
/** Throws if the `bytes` values among `attributes` add up to more than {@link MAX_ENTITY_BYTES}. */
function assertBytesFit(attributes) {
let total = 0;
for (const { typeId, value } of attributes) if ((typeId & ~INDEXED_FLAG) === TYPE_IDS.bytes) total += (value.length - 2) / 2;
if (total > MAX_ENTITY_BYTES) throw new EntityTooLargeError(total, MAX_ENTITY_BYTES);
}
/**
* The engine requires attributes **strictly ascending by their `bytes32` name**: that ordering is
* what the canonical serialization hashes into the state root, and it doubles as the uniqueness
* check.
*/
function byName(a, b) {
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
}
//#endregion
export { str as A, DECIMAL_UNIT as B, bool as C, decUnits as D, decFromUnits as E, INDEXED_FLAG as F, MissingValueError as G, EntityTooLargeError as H, TYPE_IDS as I, TooManyAttributesError as J, SchemaDefinitionError as K, isArkivValue as L, u256 as M, u64 as N, i32 as O, isColumn as P, isIndexable as R, addr as S, dec as T, InvalidAttributeNameError as U, ConflictingMutationError as V, InvalidValueError as W, UntypedValueError as X, UnknownAttributeTypeError as Y, I32_MIN as _, SCHEMA_ATTRIBUTE as a, U256_MAX as b, isSchema as c, isValidAttributeName as d, validateAttributeName as f, I32_MAX as g, decodeTypeId as h, MAX_ATTRIBUTES as i, toValue as j, key as k, lookupColumn as l, decodeRpcValue as m, encodeCreate as n, defineSchema as o, asTypeTag as p, SchemaMismatchError as q, encodePatch as r, describeColumns as s, MAX_ENTITY_BYTES as t, MAX_NAME_BYTES as u, MAX_BYTES_LENGTH as v, bytes as w, U64_MAX as x, MAX_STRING_BYTES as y, DECIMAL_SCALE as z };
//# sourceMappingURL=schema-B-qj25Fz.js.map