UNPKG

@arkiv-network/sdk

Version:

TypeScript client library for Arkiv's blockchains interactions

160 lines (136 loc) • 6.21 kB
import { describe, expect, it, vi } from "bun:test" import { encodeFunctionResult, type Hex } from "viem" import type { ArkivClient } from "../../clients/baseClient" import { ARKIV_ADDRESS } from "../../consts" import { NO_SALT, predictEntityKey } from "../../entity/key" import { ENTITY_NONCE_ABI } from "../../entity/operations" import { getEntityNonce } from "./getEntityNonce" import { predictEntityKeys } from "./predictEntityKeys" const OWNER = "0x1111111111111111111111111111111111111111" as const const CHAIN_ID = 1337 /** A client whose `eth_call` answers one nonce, and records what it was asked. */ function makeClient(nonce: bigint, options: { chain?: boolean } = {}) { const request = vi.fn(async ({ method }: { method: string }) => { if (method === "eth_call") { return encodeFunctionResult({ abi: ENTITY_NONCE_ABI, functionName: "entityNonce", result: nonce, }) } if (method === "eth_chainId") return `0x${CHAIN_ID.toString(16)}` throw new Error(`unexpected request: ${method}`) }) const client = { ...(options.chain === false ? {} : { chain: { id: CHAIN_ID } }), request, } as unknown as ArkivClient return { client, request } } /** The `eth_call` the client was asked to make. */ function ethCall(request: ReturnType<typeof vi.fn>) { const call = request.mock.calls.find(([body]) => body.method === "eth_call") return call?.[0].params[0] as { to: Hex; data: Hex } } describe("getEntityNonce", () => { it("reads the owner's minting nonce off the registry", async () => { const { client, request } = makeClient(7n) expect(await getEntityNonce(client, OWNER)).toBe(7n) // The selector is part of the node's ABI: `cast sig 'entityNonce(address)'` is 0x36917bfd. const { to, data } = ethCall(request) expect(to).toBe(ARKIV_ADDRESS) expect(data).toBe(`0x36917bfd000000000000000000000000${OWNER.slice(2)}`) }) it("reports a fresh account as zero rather than as an absence", async () => { const { client } = makeClient(0n) expect(await getEntityNonce(client, OWNER)).toBe(0n) }) }) describe("predictEntityKeys", () => { it("derives one pair per create, walking the nonce forward", async () => { const { client } = makeClient(7n) const pairs = await predictEntityKeys(client, { owner: OWNER, salts: [1n, 2n, 3n] }) expect(pairs).toEqual([ { salt: 1n, key: predictEntityKey({ owner: OWNER, nonce: 7n, salt: 1n, chainId: CHAIN_ID }) }, { salt: 2n, key: predictEntityKey({ owner: OWNER, nonce: 8n, salt: 2n, chainId: CHAIN_ID }) }, { salt: 3n, key: predictEntityKey({ owner: OWNER, nonce: 9n, salt: 3n, chainId: CHAIN_ID }) }, ]) // Distinct keys, which is the whole point of the nonce. expect(new Set(pairs.map((pair) => pair.key)).size).toBe(3) }) it("makes up the salts when only asked for a count, and pairs each with its key", async () => { const { client } = makeClient(0n) // A literal count is a tuple, so this destructures without a cast — that it compiles is the // assertion. const [first, second] = await predictEntityKeys(client, { owner: OWNER, count: 2 }) // Random salts, so the only thing worth asserting is that the keys follow from them. expect(first.key).toBe( predictEntityKey({ owner: OWNER, nonce: 0n, salt: first.salt, chainId: CHAIN_ID }), ) expect(second.key).toBe( predictEntityKey({ owner: OWNER, nonce: 1n, salt: second.salt, chainId: CHAIN_ID }), ) expect(first.salt).not.toBe(second.salt) }) it("keeps a runtime count a plain array rather than guessing its length", async () => { const { client } = makeClient(0n) const count: number = 3 const pairs = await predictEntityKeys(client, { owner: OWNER, count }) expect(pairs).toHaveLength(3) expect(pairs.map((pair) => pair.key)).toEqual( pairs.map((pair, index) => predictEntityKey({ owner: OWNER, nonce: BigInt(index), salt: pair.salt, chainId: CHAIN_ID, }), ), ) }) it("takes NO_SALT in a slot, and pairs the key with the zero it resolves to", async () => { const { client } = makeClient(4n) const [first, second] = await predictEntityKeys(client, { owner: OWNER, salts: [NO_SALT, 5n], }) expect(first).toEqual({ salt: 0n, key: predictEntityKey({ owner: OWNER, nonce: 4n, salt: 0n, chainId: CHAIN_ID }), }) expect(second).toEqual({ salt: 5n, key: predictEntityKey({ owner: OWNER, nonce: 5n, salt: 5n, chainId: CHAIN_ID }), }) }) it("prefers the salts it was given over the count", async () => { const { client } = makeClient(0n) const pairs = await predictEntityKeys(client, { owner: OWNER, count: 5, salts: [9n] }) expect(pairs).toEqual([ { salt: 9n, key: predictEntityKey({ owner: OWNER, nonce: 0n, salt: 9n, chainId: CHAIN_ID }) }, ]) }) it("asks the chain for its id when the client was built without one", async () => { const { client, request } = makeClient(0n, { chain: false }) const [only] = await predictEntityKeys(client, { owner: OWNER, salts: [1n] }) expect(request.mock.calls.some(([body]) => body.method === "eth_chainId")).toBe(true) expect(only.key).toBe( predictEntityKey({ owner: OWNER, nonce: 0n, salt: 1n, chainId: CHAIN_ID }), ) }) it("refuses a request that does not say how many keys it wants", async () => { const { client } = makeClient(0n) await expect(predictEntityKeys(client, { owner: OWNER })).rejects.toThrow(/`count` or `salts`/) await expect(predictEntityKeys(client, { owner: OWNER, salts: [] })).rejects.toThrow(/empty/) await expect(predictEntityKeys(client, { owner: OWNER, count: 0 })).rejects.toThrow( /positive whole number/, ) await expect(predictEntityKeys(client, { owner: OWNER, count: 1.5 })).rejects.toThrow( /positive whole number/, ) }) it("rejects a salt too wide for the uint128 the engine mixes in", async () => { const { client } = makeClient(0n) await expect(predictEntityKeys(client, { owner: OWNER, salts: [2n ** 128n] })).rejects.toThrow() }) })