diff --git a/src/abi/calldata/string.ts b/src/abi/calldata/string.ts index b87410c..6856003 100644 --- a/src/abi/calldata/string.ts +++ b/src/abi/calldata/string.ts @@ -7,7 +7,12 @@ function reportError(msg: string, data: CalldataEncodable): never { function toStringImplMap(data: Iterable<[string, CalldataEncodable]>, to: string[]) { to.push("{"); + let first = true; for (const [k, v] of data) { + if (!first) { + to.push(","); + } + first = false; to.push(JSON.stringify(k)); to.push(":"); toStringImpl(v, to); @@ -48,13 +53,17 @@ function toStringImplMap(data: Iterable<[string, CalldataEncodable]>, to: string if (data instanceof Uint8Array) { to.push("b#"); for (const b of data) { - to.push(b.toString(16)); + to.push(b.toString(16).padStart(2, "0")); } } else if (data instanceof Array) { to.push("["); + let first = true; for (const c of data) { + if (!first) { + to.push(","); + } + first = false; toStringImpl(c, to); - to.push(","); } to.push("]"); } else if (data instanceof Map) { @@ -62,7 +71,7 @@ function toStringImplMap(data: Iterable<[string, CalldataEncodable]>, to: string } else if (data instanceof CalldataAddress) { to.push("addr#"); for (const c of data.bytes) { - to.push(c.toString(16)); + to.push(c.toString(16).padStart(2, "0")); } } else if (Object.getPrototypeOf(data) === Object.prototype) { toStringImplMap(Object.entries(data), to); diff --git a/tests/calldata-to-string.test.ts b/tests/calldata-to-string.test.ts new file mode 100644 index 0000000..8b2af0b --- /dev/null +++ b/tests/calldata-to-string.test.ts @@ -0,0 +1,41 @@ +import {describe, it, expect} from "vitest"; +import * as calldata from "@/abi/calldata"; +import {CalldataAddress} from "@/types/calldata"; + +// Expected output matches the reference implementation in the genvm repo +// (runners/genlayer-py-std/src/genlayer/calldata/__init__.py :: to_str) + +describe("calldata.toString", () => { + it("encodes bytes as zero-padded hex", () => { + expect(calldata.toString(new Uint8Array([0xab, 0xcd]))).toBe("b#abcd"); + expect(calldata.toString(new Uint8Array([0x01, 0x02]))).toBe("b#0102"); + expect(calldata.toString(new Uint8Array([0xff]))).toBe("b#ff"); + }); + + it("does not collapse distinct byte arrays into the same string", () => { + const a = calldata.toString(new Uint8Array([0x01, 0x02])); + const b = calldata.toString(new Uint8Array([0x12])); + expect(a).not.toBe(b); + }); + + it("encodes an address as 40 hex characters", () => { + const addr = new CalldataAddress(new Uint8Array(20).fill(0x01)); + expect(calldata.toString(addr)).toBe("addr#" + "01".repeat(20)); + }); + + it("separates map entries with commas", () => { + expect(calldata.toString({a: 1})).toBe('{"a":1}'); + expect(calldata.toString({x: true, y: null})).toBe('{"x":true,"y":null}'); + expect(calldata.toString({})).toBe("{}"); + }); + + it("separates array items with commas and emits no trailing comma", () => { + expect(calldata.toString([1, 2, 3])).toBe("[1,2,3]"); + expect(calldata.toString([])).toBe("[]"); + expect(calldata.toString([[1], [2, 3]])).toBe("[[1],[2,3]]"); + }); + + it("handles nesting", () => { + expect(calldata.toString({items: [1, "two", null]})).toBe('{"items":[1,"two",null]}'); + }); +});