UNPKG

@arkiv-network/sdk

Version:

TypeScript client library for Arkiv's blockchains interactions

201 lines (177 loc) • 8.1 kB
import { describe, expect, it } from "bun:test" import type { Hex } from "viem" import { asTypeTag, decodeRpcValue, decodeTypeId, decodeValueBytes, encodeAbiAttribute, encodeTombstone, encodeValueBytes, } from "./codec" import { InvalidValueError, UnknownAttributeTypeError } from "./errors" import type { ArkivValue } from "./types" import { addr, bool, bytes, dec, i32, key, str, u64, u256 } from "./values" const ZERO = `0x${"00".repeat(32)}` as Hex const KEY = `0x${"ab".repeat(32)}` as Hex const VITALIK = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" /** One value of every type, including the awkward edges of each. */ const SAMPLES: ArkivValue[] = [ bool(true), bool(false), i32(0), i32(-42), i32(2147483647), i32(-2147483648), u64(0n), u64(2n ** 64n - 1n), u256(0n), u256(2n ** 256n - 1n), dec("0"), dec("3.5"), dec("-3.5"), dec("-0.000000000000000001"), key(KEY), addr(VITALIK), str(""), str("hello"), str("é".repeat(64)), bytes(new Uint8Array()), bytes("0xdeadbeef"), ] describe("ABI value encoding", () => { it("round-trips every type", () => { for (const value of SAMPLES) { const back = decodeValueBytes(value.type, encodeValueBytes(value)) expect(back.type).toBe(value.type) expect(back.value).toEqual(value.value) } }) it("gives every word type exactly 32 bytes", () => { for (const value of SAMPLES) { if (value.type === "str" || value.type === "bytes") continue expect(encodeValueBytes(value)).toHaveLength(2 + 64) } }) it("sign-extends a negative i32 across the whole word, as Solidity does", () => { expect(encodeValueBytes(i32(-1))).toBe(`0x${"ff".repeat(32)}`) expect(encodeValueBytes(i32(-42))).toBe(`0x${"ff".repeat(31)}d6`) expect(encodeValueBytes(i32(42))).toBe(`0x${"00".repeat(31)}2a`) }) it("stores a dec as its two's-complement scaled int256", () => { const scaled = (value: string) => BigInt.asIntN(256, BigInt(encodeValueBytes(dec(value)))) expect(scaled("1.5")).toBe(1_500_000_000_000_000_000n) expect(scaled("-1.5")).toBe(-1_500_000_000_000_000_000n) expect(scaled("0")).toBe(0n) }) it("right-aligns an address and a bool", () => { expect(encodeValueBytes(addr(VITALIK))).toBe( `0x${"00".repeat(12)}${VITALIK.slice(2).toLowerCase()}`, ) expect(encodeValueBytes(bool(true))).toBe(`0x${"00".repeat(31)}01`) expect(encodeValueBytes(bool(false))).toBe(ZERO) }) it("gives a str its raw UTF-8 bytes, with no padding", () => { expect(encodeValueBytes(str("hi"))).toBe("0x6869") expect(encodeValueBytes(str(""))).toBe("0x") // Two bytes per "é", so no relationship to a 32-byte word. expect(encodeValueBytes(str("é"))).toBe("0xc3a9") }) it("gives bytes their raw bytes, with no padding", () => { expect(encodeValueBytes(bytes("0xdeadbeef"))).toBe("0xdeadbeef") expect(encodeValueBytes(bytes(new Uint8Array()))).toBe("0x") }) it("rejects a word a well-formed encoder would not have produced", () => { expect(() => decodeValueBytes("bool", `0x${"00".repeat(31)}02`)).toThrow(/exactly 0 or 1/) expect(() => decodeValueBytes("u256", "0x2a")).toThrow(/must be 32 bytes, got 1/) // An i32 whose upper bytes contradict its sign bit. expect(() => decodeValueBytes("i32", `0xff${"00".repeat(31)}`)).toThrow(/sign-extended/) // An address with junk above its 20 bytes. expect(() => decodeValueBytes("addr", `0x${"ab".repeat(32)}`)).toThrow(/zero-padded/) // A u64 with junk above its 8 bytes — truncating it would invent a plausible block height. expect(() => decodeValueBytes("u64", `0x${"ab".repeat(32)}`)).toThrow(/zero-padded/) }) }) describe("encodeAbiAttribute", () => { it("packs the name left-aligned into its bytes32 cell", () => { const attribute = encodeAbiAttribute("level", i32(10), false) // "level" is 6c6576656c, null-padded to 32 bytes. expect(attribute.name).toBe(`0x6c6576656c${"00".repeat(27)}`) expect(attribute.typeId).toBe(2) }) it("sets bit 7 of the typeId for an indexed attribute, and only that bit", () => { expect(encodeAbiAttribute("level", i32(10), true).typeId).toBe(0x82) expect(encodeAbiAttribute("tag", str("x"), true).typeId).toBe(0x88) expect(encodeAbiAttribute("tag", str("x"), false).typeId).toBe(0x08) }) it("refuses to index bytes, which the engine would revert", () => { expect(() => encodeAbiAttribute("body", bytes("0x01"), true)).toThrow(/never indexed/) expect(encodeAbiAttribute("body", bytes("0x01"), false).typeId).toBe(7) }) it("encodes a tombstone as a zero-length value under typeId 0", () => { expect(encodeTombstone("flagged")).toEqual({ name: `0x666c6167676564${"00".repeat(25)}`, typeId: 0, value: "0x", }) }) }) describe("decodeTypeId", () => { it("splits the type from the indexed flag", () => { expect(decodeTypeId(0x88)).toEqual({ tag: "str", indexed: true }) expect(decodeTypeId(0x07)).toEqual({ tag: "bytes", indexed: false }) expect(decodeTypeId(0x87)).toEqual({ tag: "bytes", indexed: true }) }) it("rejects a type it does not know, flagged or not", () => { expect(() => decodeTypeId(0x7f)).toThrow(UnknownAttributeTypeError) expect(() => decodeTypeId(0x80)).toThrow(UnknownAttributeTypeError) }) it("reads typeId 6, the node's bytes32 type, as a key", () => { expect(decodeTypeId(0x06)).toEqual({ tag: "key", indexed: false }) expect(decodeTypeId(0x86)).toEqual({ tag: "key", indexed: true }) expect(decodeRpcValue("bytes32", KEY)).toEqual(key(KEY)) expect(asTypeTag("bytes32")).toBe("key") }) }) describe("decodeRpcValue", () => { it("reads the JSON encoding each type declares", () => { expect(decodeRpcValue("bool", true).value).toBe(true) expect(decodeRpcValue("i32", -42).value).toBe(-42) expect(decodeRpcValue("u256", "0xf4240").value).toBe(1_000_000n) expect(decodeRpcValue("dec", "3.5").value).toBe("3.5") expect(decodeRpcValue("dec", "-0.25").value).toBe("-0.25") expect(decodeRpcValue("str", "hello").value).toBe("hello") expect(decodeRpcValue("addr", VITALIK.toLowerCase()).value).toBe(VITALIK) expect(decodeRpcValue("key", KEY).value).toBe(KEY) }) it("takes an integer however it is rendered", () => { // QUANTITY is the spec's encoding; decimal strings and numbers are tolerated. expect(decodeRpcValue("u256", "0x2a").value).toBe(42n) expect(decodeRpcValue("u256", "42").value).toBe(42n) expect(decodeRpcValue("u256", 42).value).toBe(42n) expect(decodeRpcValue("i32", "-42").value).toBe(-42) }) it("decodes bytes as lowercase hex", () => { expect(decodeRpcValue("bytes", "0xDEADBEEF").value).toBe("0xdeadbeef") }) it("names the type it does not recognise, and says to upgrade", () => { expect(() => decodeRpcValue("int", "x")).toThrow(UnknownAttributeTypeError) expect(() => decodeRpcValue("int", "x")).toThrow(/upgrade @arkiv-network\/sdk/) // The pre-spec wire form: a typeId where the JSON-RPC surface names a tag. expect(() => decodeRpcValue(3 as unknown as string, "0x2a")).toThrow(UnknownAttributeTypeError) }) it("does not mistake an inherited Object key for a type tag", () => { // `type in TYPE_IDS` walks the prototype chain, so these passed the check and then fell off // the end of the exhaustive switch, returning `undefined` — the caller got a TypeError from // `entity.attributes.foo.value` instead of "upgrade the SDK". for (const inherited of ["constructor", "toString", "valueOf", "hasOwnProperty"]) { expect(() => decodeRpcValue(inherited, 1)).toThrow(UnknownAttributeTypeError) } }) it("rejects a value whose JSON shape contradicts its declared type", () => { expect(() => decodeRpcValue("u256", "not a number")).toThrow(InvalidValueError) expect(() => decodeRpcValue("str", 42)).toThrow(/expected a string/) expect(() => decodeRpcValue("bool", "yes")).toThrow(/not a boolean/) expect(() => decodeRpcValue("i32", 1.5)).toThrow(/not an integer/) }) })