@arkiv-network/sdk
Version:
TypeScript client library for Arkiv's blockchains interactions
154 lines (145 loc) • 6.13 kB
text/typescript
import type { Address, Hex } from "viem"
import { getChainId } from "viem/actions"
import type { ArkivClient } from "../../clients/baseClient"
import {
NO_SALT,
predictEntityKey,
randomSalt,
resolveSalt,
type SaltInput,
} from "../../entity/key"
import { getEntityNonce } from "./getEntityNonce"
/**
* A key a create will be given, together with the salt that mints it.
*
* The two travel as a pair because neither is any use alone: a create defaults to a *random* salt,
* so the key only comes out as predicted if that same create carries this salt.
*/
export type PredictedEntityKey = {
/** The key the create will be given. */
key: Hex
/**
* The salt the create must carry for its key to come out as {@link PredictedEntityKey.key}.
* Always a plain `uint128` — a requested {@link NO_SALT} comes back as the `0n` it resolves to,
* which a create accepts just the same.
*/
salt: bigint
}
/** Parameters for `client.predictEntityKeys`. */
export type PredictEntityKeysParameters<
TCount extends number = number,
TSalts extends readonly SaltInput[] | undefined = readonly SaltInput[] | undefined,
> = {
/** The account that will sign the creates. Its nonce is what the keys are derived from. */
owner: Address
/**
* How many creates the batch will carry. Ignored when `salts` is given, which already says how
* many there are.
*
* A literal count comes back as a fixed-length tuple, so `count: 2` destructures into two named
* pairs without a cast.
*/
count?: TCount | undefined
/**
* The salt each create will carry, in batch order. Defaults to `count` fresh random salts —
* either way each key comes back paired with the salt that mints it. Pass {@link NO_SALT} in a
* slot whose create opts out of salting.
*/
salts?: TSalts
}
/**
* The keys a batch would mint, in batch order, each paired with its salt.
*
* The length follows the request whenever it is known statically: a literal `count`, or a `salts`
* tuple, gives a fixed-length tuple; a count computed at runtime gives a plain array.
*
* `TSalts` is wrapped in a tuple to stop the conditional distributing over it. Left naked, the
* `readonly SaltInput[] | undefined` default would take both branches and union a tuple with an
* array, which widens the length straight back to `number`.
*/
export type PredictEntityKeysReturnType<
TCount extends number = number,
TSalts extends readonly SaltInput[] | undefined = readonly SaltInput[] | undefined,
> = [TSalts] extends [readonly SaltInput[]]
? { -readonly [Index in keyof TSalts]: PredictedEntityKey }
: Repeat<PredictedEntityKey, TCount>
/**
* A tuple of `TLength` × `T`, or a plain array when the length is only known at runtime.
*
* Written tail-recursively so TypeScript can build the batch sizes anyone would actually send.
*/
type Repeat<T, TLength extends number, TAcc extends T[] = []> = number extends TLength
? T[]
: TAcc["length"] extends TLength
? TAcc
: Repeat<T, TLength, [...TAcc, T]>
/**
* 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,
* },
* ],
* })
*/
export async function predictEntityKeys<
const TCount extends number = number,
const TSalts extends readonly SaltInput[] | undefined = undefined,
>(
client: ArkivClient,
{ owner, count, salts }: PredictEntityKeysParameters<TCount, TSalts>,
): Promise<PredictEntityKeysReturnType<TCount, TSalts>> {
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,
})) as PredictEntityKeysReturnType<TCount, TSalts>
}
/** The salts to derive from: the ones given, or `count` fresh random ones. */
function resolveSalts(
count: number | undefined,
salts: readonly SaltInput[] | undefined,
): bigint[] {
if (salts !== undefined) {
if (salts.length === 0) {
throw new Error("predictEntityKeys: `salts` is empty — there is nothing to predict")
}
return salts.map(resolveSalt)
}
if (count === undefined) {
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())
}