From f394165218dcbb26651cc752bc5a5c0a4db2a29e Mon Sep 17 00:00:00 2001 From: mister Date: Thu, 9 Jul 2026 16:37:52 +0200 Subject: [PATCH] fix: allow contract view calls without execute --- .../skills/upvoting-on-abstract/SKILL.md | 14 +++--- .../src/execution/handlers/contract.ts | 23 ++++++++- test/agw-runtime.test.ts | 47 +++++++++++++++++++ 3 files changed, 76 insertions(+), 8 deletions(-) diff --git a/packages/agw-cli/skills/upvoting-on-abstract/SKILL.md b/packages/agw-cli/skills/upvoting-on-abstract/SKILL.md index 39c6e5b..b27f06f 100644 --- a/packages/agw-cli/skills/upvoting-on-abstract/SKILL.md +++ b/packages/agw-cli/skills/upvoting-on-abstract/SKILL.md @@ -31,7 +31,7 @@ The AGW CLI requires full JSON ABI objects, not human-readable strings. Every `a ### Check vote cost and current epoch ```bash -agw contract write --json '{ +agw-cli contract write --json '{ "address": "0x3b50de27506f0a8c1f4122a1e6f470009a76ce2a", "abi": [{"type":"function","name":"voteCost","stateMutability":"view","inputs":[],"outputs":[{"name":"","type":"uint96"}]}], "functionName": "voteCost", @@ -40,7 +40,7 @@ agw contract write --json '{ ``` ```bash -agw contract write --json '{ +agw-cli contract write --json '{ "address": "0x3b50de27506f0a8c1f4122a1e6f470009a76ce2a", "abi": [{"type":"function","name":"currentEpoch","stateMutability":"view","inputs":[],"outputs":[{"name":"","type":"uint256"}]}], "functionName": "currentEpoch", @@ -51,7 +51,7 @@ agw contract write --json '{ ### Check remaining votes ```bash -agw contract write --json '{ +agw-cli contract write --json '{ "address": "0x3b50de27506f0a8c1f4122a1e6f470009a76ce2a", "abi": [{"type":"function","name":"userVotesRemaining","stateMutability":"view","inputs":[{"name":"user","type":"address"}],"outputs":[{"name":"","type":"uint256"}]}], "functionName": "userVotesRemaining", @@ -64,7 +64,7 @@ agw contract write --json '{ Replace `` with the Portal app ID (e.g., `25` for Onchain Heroes) and `` with the value from `voteCost()`: ```bash -agw contract write --json '{ +agw-cli contract write --json '{ "address": "0x3b50de27506f0a8c1f4122a1e6f470009a76ce2a", "abi": [{"type":"function","name":"voteForApp","stateMutability":"payable","inputs":[{"name":"appId","type":"uint256"}],"outputs":[]}], "functionName": "voteForApp", @@ -78,7 +78,7 @@ Execute only after confirming the preview: replace `--dry-run` with `--execute`. ### Check votes for a specific app ```bash -agw contract write --json '{ +agw-cli contract write --json '{ "address": "0x3b50de27506f0a8c1f4122a1e6f470009a76ce2a", "abi": [{"type":"function","name":"getVotesForApp","stateMutability":"view","inputs":[{"name":"appId","type":"uint256"},{"name":"epoch","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]}], "functionName": "getVotesForApp", @@ -91,7 +91,7 @@ Use `currentEpoch()` to get the current epoch number first. ### Check which apps a user voted for ```bash -agw contract write --json '{ +agw-cli contract write --json '{ "address": "0x3b50de27506f0a8c1f4122a1e6f470009a76ce2a", "abi": [{"type":"function","name":"getUserVotes","stateMutability":"view","inputs":[{"name":"user","type":"address"},{"name":"epoch","type":"uint256"}],"outputs":[{"name":"","type":"uint256[]"}]}], "functionName": "getUserVotes", @@ -105,7 +105,7 @@ Returns an array of app IDs the user has voted for this epoch. 1. Check vote cost: `voteCost()` → returns cost in wei 2. Check remaining votes: `userVotesRemaining(address)` → returns count -3. Find the app ID via `agw portal apps list` or `agw app list` +3. Find the app ID via `agw-cli app list` 4. Preview the vote: `voteForApp(appId)` with `--dry-run` and `value` set to vote cost 5. Execute after confirmation: `--execute` 6. Verify: `getUserVotes(address, epoch)` to confirm your vote was recorded diff --git a/packages/agw-core/src/execution/handlers/contract.ts b/packages/agw-core/src/execution/handlers/contract.ts index 04c1b22..99b265c 100644 --- a/packages/agw-core/src/execution/handlers/contract.ts +++ b/packages/agw-core/src/execution/handlers/contract.ts @@ -1,4 +1,4 @@ -import { encodeFunctionData } from "viem"; +import { encodeFunctionData, type Abi, type AbiFunction } from "viem"; import { AgwCliError } from "../../errors.js"; import { assertToolAllowedByPolicyMeta } from "../../policy/meta.js"; import { deployContractTool } from "../../tools/deploy-contract.js"; @@ -7,6 +7,14 @@ import type { CommandHandler } from "../types.js"; import { requireActiveSession } from "../context.js"; import { assertAddressString, assertDecimalString, parseOptionalBoolean } from "../validation.js"; +function isViewOrPure(abi: Abi, functionName: string): boolean { + const fn = abi.find( + (item): item is AbiFunction => + item.type === "function" && item.name === functionName, + ); + return fn?.stateMutability === "view" || fn?.stateMutability === "pure"; +} + export const contractHandlers: Record = { "contract.write": async (input, context) => { const session = requireActiveSession(context); @@ -36,6 +44,19 @@ export const contractHandlers: Record = { throw new AgwCliError("INVALID_INPUT", "invalid abi/functionName/args payload", 2); } + if (isViewOrPure(input.abi as Abi, input.functionName)) { + return writeContractTool.handler( + { + address: input.address, + abi: input.abi, + functionName: input.functionName, + args: input.args, + value: input.value, + }, + context, + ); + } + return { preview: true, requiresExplicitExecute: true, diff --git a/test/agw-runtime.test.ts b/test/agw-runtime.test.ts index 0868da6..621fc88 100644 --- a/test/agw-runtime.test.ts +++ b/test/agw-runtime.test.ts @@ -406,6 +406,53 @@ describe("agw runtime", () => { }); }); + it("executes view contract calls without requiring execute", async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "agw-runtime-")); + createActiveSession(tmpDir); + + global.fetch = jest.fn(async () => + new Response( + JSON.stringify({ + jsonrpc: "2.0", + id: 1, + result: "0x000000000000000000000000000000000000000000000000000000000000007b", + }), + { status: 200, headers: { "content-type": "application/json" } }, + ), + ) as unknown as typeof fetch; + + await expect( + executeCommand("contract.write", { + address: "0x4444444444444444444444444444444444444444", + abi: [ + { + type: "function", + name: "currentEpoch", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "uint256" }], + }, + ], + functionName: "currentEpoch", + args: [], + }, { + homeDir: tmpDir, + }), + ).resolves.toEqual({ + outputMode: "json", + result: { + result: "123", + accountAddress: "0x1111111111111111111111111111111111111111", + chainId: 2741, + contract: { + address: "0x4444444444444444444444444444444444444444", + functionName: "currentEpoch", + args: [], + }, + }, + }); + }); + it("defaults pagination-aware pageAll reads to ndjson when stdout is not a tty", async () => { global.fetch = jest.fn(async () => new Response(