Skip to content
Merged
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
23 changes: 20 additions & 3 deletions src/tools.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -947,7 +947,16 @@ export async function runAction(a, resolved, opts = {}) {
const bal = await wallet.getBalance();
return `${formatMon(bal)} ${SYMBOL()}`;
}
const token = resolveToken(input);
let token;
try {
token = resolveToken(input);
} catch (err) {
const detail = err?.shortMessage || err?.message || "invalid token identifier";
if (/checksum/i.test(detail)) {
return `Refused: invalid token address "${safeEcho(input)}" (checksum failed)`;
}
return `Refused: invalid token "${safeEcho(input)}": ${safeEcho(detail)}`;
}
if (!token) {
return unknownTokenMessage(input);
}
Expand Down Expand Up @@ -1012,8 +1021,16 @@ export async function runAction(a, resolved, opts = {}) {

case "get_nfts": {
const owner = a.address ?? a.owner;
if (owner && !isAddress(owner)) {
return `Refused: "${String(owner).trim()}" is not a valid address.`;
if (owner) {
const trimmed = String(owner).trim();
if (!isAddress(trimmed)) {
return `Refused: "${safeEcho(trimmed)}" is not a valid address.`;
}
try {
toChecksumAddress(trimmed);
} catch {
return `Refused: "${safeEcho(trimmed)}" is not a valid address (checksum failed)`;
}
}
const { tokens, skipped, truncated } = await wallet.getNfts(owner);
// Notes, not silence: an empty list caused by unusable rows must not read as "you own
Expand Down
11 changes: 10 additions & 1 deletion test/nft.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { parseAction, describeAction, runAction, isWrite, ACTIONS, systemPrompt } from "../src/tools.mjs";
import { parseAction, describeAction, runAction, isWrite, ACTIONS, systemPrompt, isRefusal } from "../src/tools.mjs";
import { buildNftTransferCalldata, ERC721_ABI, normalizeNftPage, transferNft } from "../src/wallet.mjs";
import { Interface, getAddress } from "ethers";
import { config } from "../src/config.mjs";
Expand Down Expand Up @@ -184,6 +184,15 @@ describe("runAction — get_nfts / transfer_nft guards", () => {
assert.match(String(res), /refused/i);
});

it("get_nfts with bad address checksum returns refusal instead of throwing", async () => {
const BAD = "0x534B2f3A21130D7A60830C2Df862319E593943A3";
const res = await runAction({ action: "get_nfts", address: BAD });
assert.equal(isRefusal(res), true);
assert.match(String(res), /^Refused:/);
assert.match(String(res), /checksum failed/);
assert.match(String(res), new RegExp(BAD));
});

it("transfer_nft with invalid to returns refusal", async () => {
const res = await runAction({ action: "transfer_nft", contractAddress: "0xabc", tokenId: "1", to: "not-an-address" });
assert.match(String(res), /refused/i);
Expand Down
11 changes: 10 additions & 1 deletion test/tools.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { describe, it, test } from "node:test";
import assert from "node:assert/strict";
import { ACTIONS, parseAction, isWrite, describeAction, systemPrompt, runAction } from "../src/tools.mjs";
import { ACTIONS, parseAction, isWrite, describeAction, systemPrompt, runAction, isRefusal } from "../src/tools.mjs";
import { config } from "../src/config.mjs";

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -342,6 +342,15 @@ test("unknown token balance errors explain when the catalog is empty", async ()
}
});

test("get_token_balance refuses a token address with a bad checksum instead of throwing", async () => {
const BAD = "0x534B2f3A21130D7A60830C2Df862319E593943A3";
const result = await runAction({ action: "get_token_balance", token: BAD });
assert.equal(isRefusal(result), true);
assert.match(result, /^Refused:/);
assert.match(result, /checksum failed/);
assert.match(result, new RegExp(BAD));
});

// ─── Native tool-calling tests ───────────────────────────────────────────────
import { getToolDefinitions, dispatchToolCall } from "../src/tools.mjs";

Expand Down
Loading