Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/abi/calldata/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -48,21 +53,25 @@ 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) {
toStringImplMap(data.entries(), to);
} 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);
Expand Down
41 changes: 41 additions & 0 deletions tests/calldata-to-string.test.ts
Original file line number Diff line number Diff line change
@@ -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]}');
});
});