diff --git a/src/commands/contracts/index.ts b/src/commands/contracts/index.ts index f4909eb3..361c8cc0 100644 --- a/src/commands/contracts/index.ts +++ b/src/commands/contracts/index.ts @@ -8,13 +8,13 @@ import {CodeAction, CodeOptions} from "./code"; import {EstimateFeesAction, EstimateFeesOptions} from "./estimateFees"; import {addWalletModeOption} from "../../lib/wallet/walletOption"; -const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/; -const ADDR_PREFIX_RE = /^addr#([0-9a-fA-F]{40})$/; -const BYTES_PREFIX_RE = /^b#([0-9a-fA-F]*)$/; -const HEX_RE = /^0x[0-9a-fA-F]+$/; +const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/i; +const ADDR_PREFIX_RE = /^addr#([0-9a-fA-F]{40})$/i; +const BYTES_PREFIX_RE = /^b#([0-9a-fA-F]*)$/i; +const HEX_RE = /^0x[0-9a-fA-F]+$/i; function hexToBytes(hex: string): Uint8Array { - const bytes = new Uint8Array(hex.length / 2); + const bytes = new Uint8Array(Math.floor(hex.length / 2)); for (let i = 0; i < bytes.length; i++) { bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); } @@ -50,11 +50,17 @@ export function parseScalar(value: string): unknown { if (ADDRESS_RE.test(value)) return new CalldataAddress(hexToBytes(value.slice(2))); const bytesMatch = value.match(BYTES_PREFIX_RE); - if (bytesMatch) return hexToBytes(bytesMatch[1]); + if (bytesMatch) { + if (bytesMatch[1].length % 2 !== 0) { + return value; + } + return hexToBytes(bytesMatch[1]); + } if (HEX_RE.test(value)) return BigInt(value); - if (!isNaN(Number(value)) && Number.isSafeInteger(Number(value))) return Number(value); - if (!isNaN(Number(value))) return BigInt(value); + if (value.trim() !== "" && !isNaN(Number(value)) && Number.isSafeInteger(Number(value))) + return Number(value); + if (value.trim() !== "" && !isNaN(Number(value))) return BigInt(value); return value; } @@ -122,14 +128,14 @@ export function initializeContractsCommands(program: Command) { .option("--valid-until ", "Unix timestamp after which the transaction is invalid") .option("--args ", ARGS_HELP, parseArg, []), ).action(async (options: DeployOptions) => { - const deployer = new DeployAction(); - if (options.contract) { - await deployer.deploy(options); - } else { - const deployScriptsOptions: DeployScriptsOptions = {rpc: options.rpc}; - await deployer.deployScripts(deployScriptsOptions); - } - }); + const deployer = new DeployAction(); + if (options.contract) { + await deployer.deploy(options); + } else { + const deployScriptsOptions: DeployScriptsOptions = {rpc: options.rpc}; + await deployer.deployScripts(deployScriptsOptions); + } + }); program .command("call ") @@ -154,9 +160,9 @@ export function initializeContractsCommands(program: Command) { .option("--valid-until ", "Unix timestamp after which the transaction is invalid") .option("--args ", ARGS_HELP, parseArg, []), ).action(async (contractAddress: string, method: string, options: WriteOptions) => { - const writeAction = new WriteAction(); - await writeAction.write({contractAddress, method, ...options}); - }); + const writeAction = new WriteAction(); + await writeAction.write({contractAddress, method, ...options}); + }); program .command("estimate-fees [contractAddress] [method]") diff --git a/tests/commands/parseArg.test.ts b/tests/commands/parseArg.test.ts index 28d70146..df649a6d 100644 --- a/tests/commands/parseArg.test.ts +++ b/tests/commands/parseArg.test.ts @@ -66,6 +66,25 @@ describe("parseScalar", () => { expect(parseScalar("someString")).toBe("someString"); }); + test("parses empty string and whitespace strings without numeric coercion to 0", () => { + expect(parseScalar("")).toBe(""); + expect(parseScalar(" ")).toBe(" "); + expect(parseScalar("\t")).toBe("\t"); + }); + + test("parses uppercase hex and addresses case-insensitively", () => { + expect(parseScalar("0X1234")).toBe(BigInt("0x1234")); + const upperAddr = parseScalar("0X6857Ed54CbafaA74Fc0357145eC0ee1536ca45A0"); + expect(upperAddr).toBeInstanceOf(CalldataAddress); + const prefixAddr = parseScalar("ADDR#6857Ed54CbafaA74Fc0357145eC0ee1536ca45A0"); + expect(prefixAddr).toBeInstanceOf(CalldataAddress); + }); + + test("handles odd-length byte prefix without byte truncation", () => { + expect(parseScalar("b#abc")).toBe("b#abc"); + expect(parseScalar("B#deadbeef")).toBeInstanceOf(Uint8Array); + }); + test("does not treat invalid hex as address", () => { const invalid = "0xZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"; expect(parseScalar(invalid)).toBe(invalid); @@ -103,6 +122,11 @@ describe("coerceValue", () => { const result = coerceValue(["0x6857Ed54CbafaA74Fc0357145eC0ee1536ca45A0"]) as any[]; expect(result[0]).toBeInstanceOf(CalldataAddress); }); + + test("preserves empty strings in objects and arrays without coercing to zero", () => { + expect(coerceValue({name: ""})).toEqual({name: ""}); + expect(coerceValue(["", "non-empty"])).toEqual(["", "non-empty"]); + }); }); describe("parseArg", () => { @@ -112,6 +136,11 @@ describe("parseArg", () => { expect(parseArg("hello")).toEqual(["hello"]); }); + test("parses empty string argument as a string rather than zero", () => { + expect(parseArg("")).toEqual([""]); + expect(parseArg(" ")).toEqual([" "]); + }); + test("parses null", () => { expect(parseArg("null")).toEqual([null]); }); @@ -126,6 +155,11 @@ describe("parseArg", () => { expect(result).toEqual([{key: "value", n: 42}]); }); + test("parses JSON with empty strings preserving string types", () => { + const result = parseArg('{"key": "", "count": 0}'); + expect(result).toEqual([{key: "", count: 0}]); + }); + test("parses nested JSON with address strings", () => { const result = parseArg('{"to": "0x6857Ed54CbafaA74Fc0357145eC0ee1536ca45A0", "amount": 100}'); expect(result).toHaveLength(1);