@arkiv-network/sdk
Version:
TypeScript client library for Arkiv's blockchains interactions
121 lines (107 loc) • 4.13 kB
text/typescript
import { checksumAddress, type Hex } from "viem"
import {
decodeCreationFlags,
encodeCreationFlags,
type ResolvedCreationFlags,
} from "../entity/flags"
import type { AttributeSchema, Attributes } from "../schema"
// Internal seam: the JSON-RPC decoder is not part of the package's public surface.
import { asTypeTag, decodeRpcValue } from "../schema/codec"
import { Entity, type EntityFields } from "../types/entity"
import type { RpcEntity } from "../types/rpcSchema"
import { getLogger } from "./logger"
const logger = getLogger("utils:entities")
/**
* Turns the attribute array a query returns into a name-keyed map of typed values.
*
* Attribute names are unique per entity, so the map loses nothing and gives callers
* `entity.attributes.level.value` instead of a linear search.
*/
function decodeAttributes(rpcAttributes: NonNullable<RpcEntity["attributes"]>): Attributes {
const attributes: Record<string, ReturnType<typeof decodeRpcValue>> = {}
for (const { name, type, value } of rpcAttributes) {
attributes[name] = decodeRpcValue(type, value)
}
return attributes
}
/** The same, for the values-free `attributeSchema` projection. */
function decodeSchema(entries: NonNullable<RpcEntity["attributeSchema"]>): AttributeSchema {
const schema: Record<string, AttributeSchema[string]> = {}
for (const { name, type, indexed } of entries) {
schema[name] = {
type: asTypeTag(type),
indexed: typeof indexed === "boolean" ? indexed : undefined,
}
}
return schema
}
/**
* Builds an {@link Entity} from a query result row.
*
* A field the query did not select is absent from the response and stays `undefined` here — the
* decoder never substitutes an empty value for one that was not asked for.
*/
export function entityFromRpcResult(rpcEntity: RpcEntity): Entity {
logger("entityFromRpcResult %o", rpcEntity)
const fields: EntityFields = {
key: rpcEntity.key,
owner: toAddress(rpcEntity.owner),
creator: toAddress(rpcEntity.creator),
createdAt: toBlock(rpcEntity.createdAt),
updatedAt: toBlock(rpcEntity.updatedAt),
expiresAt: toBlock(rpcEntity.expiresAt),
creationFlags: decodeFlags(rpcEntity.creationFlags),
attributeSchema:
rpcEntity.attributeSchema !== undefined ? decodeSchema(rpcEntity.attributeSchema) : undefined,
attributes:
rpcEntity.attributes !== undefined ? decodeAttributes(rpcEntity.attributes) : undefined,
}
return new Entity(fields)
}
function toBlock(value: string | undefined): bigint | undefined {
return value === undefined ? undefined : BigInt(value)
}
function toAddress(value: Hex | undefined): Hex | undefined {
if (value === undefined) return undefined
if (!/^0x[0-9a-fA-F]{40}$/.test(value)) {
logger("unreadable address %o", value)
return value
}
return checksumAddress(value)
}
/**
* Resolves the `creationFlags` projection, preferring the byte.
*/
function decodeFlags(
value: NonNullable<RpcEntity["creationFlags"]> | undefined,
): ResolvedCreationFlags | undefined {
if (value === undefined) return undefined
const raw = toFlagsByte(typeof value === "number" ? value : value.raw)
if (raw !== undefined) return decodeCreationFlags(raw)
if (
typeof value === "object" &&
(value.readonly ?? value.permissionlessExtension) !== undefined
) {
return decodeCreationFlags(
encodeCreationFlags({
readonly: value.readonly ?? false,
permissionlessExtension: value.permissionlessExtension ?? false,
}),
)
}
logger("unreadable creationFlags %o", value)
return undefined
}
/**
* A flags byte in whichever spelling arrived: a JSON number, or the `0x` QUANTITY that every other
* numeric field on {@link RpcEntity} already uses. `undefined` for anything that is not a byte.
*/
function toFlagsByte(value: unknown): number | undefined {
const parsed =
typeof value === "number"
? value
: typeof value === "string" && /^(0x[0-9a-fA-F]+|\d+)$/.test(value)
? Number(value)
: Number.NaN
return Number.isInteger(parsed) && parsed >= 0 && parsed <= 0xff ? parsed : undefined
}