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
14 changes: 7 additions & 7 deletions packages/agw-cli/skills/upvoting-on-abstract/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -64,7 +64,7 @@ agw contract write --json '{
Replace `<APP_ID>` with the Portal app ID (e.g., `25` for Onchain Heroes) and `<VOTE_COST>` 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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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
Expand Down
23 changes: 22 additions & 1 deletion packages/agw-core/src/execution/handlers/contract.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<string, CommandHandler> = {
"contract.write": async (input, context) => {
const session = requireActiveSession(context);
Expand Down Expand Up @@ -36,6 +44,19 @@ export const contractHandlers: Record<string, CommandHandler> = {
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,
Expand Down
47 changes: 47 additions & 0 deletions test/agw-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down