From 698f3bfdb48c72bd54d68ea040a582043bef2135 Mon Sep 17 00:00:00 2001 From: memosr Date: Sat, 22 Aug 2026 21:37:02 +0300 Subject: [PATCH] fix(calldata): align toString output with the reference format `abi.calldata.toString` produced output that does not match the reference implementation in genvm (runners/genlayer-py-std, calldata/__init__.py::to_str). Three defects: - Bytes and addresses were serialised with `toString(16)` and no zero padding, so `0x0a` became `a`. This is lossy: `Uint8Array([0x01, 0x02])` and `Uint8Array([0x12])` both rendered as `b#12`, and a 20-byte address could render as fewer than 40 hex characters. - Map entries had no separator at all, producing `{"a":1"b":2}`. - Arrays emitted a trailing comma, producing `[1,2,3,]`. The function is public API and also feeds the `readable` field of `decodeTransaction` / `simplifyTransactionReceipt` via `calldataToUserFriendlyJson`, so the malformed output is user visible. Adds tests mirroring the reference test suite (runners/genlayer-py-std/tests/test_calldata_to_str.py). Note: `string.ts` does not satisfy prettier on `v1` either; left untouched to keep the diff reviewable. --- src/abi/calldata/string.ts | 15 +++++++++--- tests/calldata-to-string.test.ts | 41 ++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 tests/calldata-to-string.test.ts 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]}'); + }); +});