UNPKG

@arkiv-network/sdk

Version:

TypeScript client library for Arkiv's blockchains interactions

208 lines (184 loc) • 7.82 kB
import { describe, expect, it } from "bun:test" import { decodeAbiParameters, parseAbiParameters, toHex } from "viem" import { encodeCreate } from "../schema/attributes" import { defineSchema } from "../schema/schema" import { bytes, dec, i32, str, u256 } from "../schema/values" import { InvalidCreationFlagsError } from "./errors" import { decodeCreationFlags, encodeCreationFlags } from "./flags" import { MAX_SALT, NO_SALT, predictEntityKey, randomSalt, resolveSalt, validateSalt } from "./key" import { createOperation } from "./operations" import { OperationType } from "./params" const OWNER = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" const CREATE_PARAMS = parseAbiParameters( "(uint128 salt, uint64 expiresAt, uint64 minLifetime, uint8 creationFlags, (bytes32 name, uint8 typeId, bytes value)[] attributes)", ) /** Decodes an operation's payload back into the Create struct it encodes. */ function decodeCreate(operationData: `0x${string}`) { const [create] = decodeAbiParameters(CREATE_PARAMS, operationData) return create } describe("creation flags", () => { it("packs each flag into its documented bit", () => { expect(encodeCreationFlags({})).toBe(0b00) expect(encodeCreationFlags({ readonly: true })).toBe(0b01) expect(encodeCreationFlags({ permissionlessExtension: true })).toBe(0b10) expect(encodeCreationFlags({ readonly: true, permissionlessExtension: true })).toBe(0b11) expect(encodeCreationFlags()).toBe(0) }) it("round-trips through the byte", () => { for (const readonly of [true, false]) { for (const permissionlessExtension of [true, false]) { const decoded = decodeCreationFlags( encodeCreationFlags({ readonly, permissionlessExtension }), ) expect(decoded.readonly).toBe(readonly) expect(decoded.permissionlessExtension).toBe(permissionlessExtension) } } }) it("refuses a value that is not a byte at all", () => { // Malformed input expect(() => decodeCreationFlags(256)).toThrow(InvalidCreationFlagsError) expect(() => decodeCreationFlags(256)).toThrow(/not a byte/) expect(() => decodeCreationFlags(-1)).toThrow(/not a byte/) }) it("keeps the raw byte, so unknown state stays visible", () => { expect(decodeCreationFlags(0b11).raw).toBe(3) }) }) describe("salt", () => { it("generates 128 bits within the wire field", () => { for (let i = 0; i < 32; i++) { const salt = randomSalt() expect(salt).toBeGreaterThanOrEqual(0n) expect(salt).toBeLessThanOrEqual(MAX_SALT) } }) it("does not repeat", () => { const seen = new Set(Array.from({ length: 64 }, () => randomSalt().toString())) expect(seen.size).toBe(64) }) it("bounds a caller-supplied salt to the uint128 field", () => { expect(validateSalt(0n)).toBe(0n) expect(validateSalt(MAX_SALT)).toBe(MAX_SALT) expect(() => validateSalt(MAX_SALT + 1n)).toThrow(/uint128/) expect(() => validateSalt(-1n)).toThrow(/uint128/) }) it("resolves NO_SALT to the zero, and passes anything else through", () => { expect(resolveSalt(NO_SALT)).toBe(0n) expect(resolveSalt(0n)).toBe(0n) expect(resolveSalt(42n)).toBe(42n) expect(() => resolveSalt(MAX_SALT + 1n)).toThrow(/uint128/) }) it("reports a salt it cannot use without choking on a symbol", () => { // @ts-expect-error expect(() => validateSalt(Symbol("nope"))).toThrow(/Invalid salt/) }) }) describe("predictEntityKey", () => { it("is deterministic in owner, nonce and salt", () => { const base = { owner: OWNER, nonce: 0n, salt: 7n, chainId: 1 } as const expect(predictEntityKey(base)).toBe(predictEntityKey(base)) expect(predictEntityKey({ ...base, nonce: 1n })).not.toBe(predictEntityKey(base)) expect(predictEntityKey({ ...base, salt: 8n })).not.toBe(predictEntityKey(base)) expect( predictEntityKey({ ...base, owner: "0x1111111111111111111111111111111111111111" }), ).not.toBe(predictEntityKey(base)) }) it("derives the same key from NO_SALT as from an explicit zero", () => { const base = { owner: OWNER, nonce: 3n, chainId: 1 } as const expect(predictEntityKey({ ...base, salt: NO_SALT })).toBe( predictEntityKey({ ...base, salt: 0n }), ) }) it("separates chains through the domain", () => { const base = { owner: OWNER, nonce: 0n, salt: 0n } as const expect(predictEntityKey({ ...base, chainId: 1 })).not.toBe( predictEntityKey({ ...base, chainId: 2 }), ) }) it("returns a 32-byte key", () => { expect(predictEntityKey({ owner: OWNER, nonce: 0n, salt: 0n, chainId: 1 })).toMatch( /^0x[0-9a-f]{64}$/, ) }) }) describe("the Create operation", () => { const build = (over: Partial<Parameters<typeof createOperation>[0]> = {}) => createOperation({ salt: 42n, expiry: { expiresAt: 0n, minLifetime: 43_200n }, creationFlags: 0, attributes: [], ...over, }) it("carries the CREATE tag", () => { expect(build().operation).toBe(OperationType.Create) }) it("round-trips its fields through the ABI", () => { const create = decodeCreate( build({ salt: 42n, expiry: { expiresAt: 1_200_000n, minLifetime: 100n }, creationFlags: 0b11, }).operationData, ) expect(create.salt).toBe(42n) expect(create.expiresAt).toBe(1_200_000n) expect(create.minLifetime).toBe(100n) expect(create.creationFlags).toBe(3) }) it("carries attributes as (name, typeId, bytes), sorted ascending by name", () => { const schema = defineSchema({ zebra: i32(), apple: str(), mango: u256() }) const create = decodeCreate( build({ attributes: encodeCreate(schema, { zebra: i32(1), apple: str("x"), mango: u256(2n) }), }).operationData, ) const names = create.attributes.map((a) => a.name) expect(names).toEqual([...names].sort()) expect(create.attributes.map((a) => a.typeId)).toEqual([8, 4, 2]) // apple str, mango u256, zebra i32 }) it("sets bit 7 of the typeId on indexed columns, and only on them", () => { const schema = defineSchema({ level: i32().indexed(), note: str(), body: bytes() }) const create = decodeCreate( build({ attributes: encodeCreate(schema, { level: 10, note: "hi", body: new Uint8Array([0xde, 0xad]), }), }).operationData, ) const byName = Object.fromEntries(create.attributes.map((a) => [a.name, a])) expect(byName[toHex("level", { size: 32 })].typeId).toBe(0x80 | 2) expect(byName[toHex("note", { size: 32 })].typeId).toBe(8) expect(byName[toHex("body", { size: 32 })]).toMatchObject({ typeId: 7, value: "0xdead" }) }) it("carries no cells at all for an entity with no columns", () => { const create = decodeCreate( build({ attributes: encodeCreate(defineSchema({}), {}) }).operationData, ) expect(create.attributes).toHaveLength(0) }) it("encodes each value type at its documented width", () => { const schema = defineSchema({ n: i32(), big: u256(), d: dec(), s: str() }) const create = decodeCreate( build({ attributes: encodeCreate(schema, { n: i32(-42), big: u256(1n), d: dec("1.5"), s: str("hi"), }), }).operationData, ) const byName = Object.fromEntries(create.attributes.map((a) => [a.name, a.value])) // Word types are exactly 32 bytes; i32 is sign-extended. expect(byName[toHex("n", { size: 32 })]).toBe(`0x${"ff".repeat(31)}d6`) expect(byName[toHex("big", { size: 32 })]).toBe(`0x${"00".repeat(31)}01`) expect(BigInt(byName[toHex("d", { size: 32 })])).toBe(1_500_000_000_000_000_000n) // str is raw UTF-8, not padded. expect(byName[toHex("s", { size: 32 })]).toBe("0x6869") }) })