diff --git a/CHANGELOG.md b/CHANGELOG.md index e2f3fe7..7a2d830 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ ## Unreleased +### Added + +- Built-in mapped write metadata for GBM auction commands (`auction bid`, `auction buy-now`, `auction cancel`, `auction create`, `auction swap-bid`): + - default contract address: `0x80320a0000c7a6a34086e2acad6915ff57ffda31` + - built-in ABI signatures for mapped function encoding +- Mapped help for auction writes now prints built-in ABI signature details without requiring `--abi-file`. + +### Changed + +- Mapped write execution now auto-uses built-in ABI/address defaults when available, while still allowing explicit `--abi-file` / `--address` overrides. + ## 0.2.3 - 2026-02-27 ### Fixed diff --git a/README.md b/README.md index bbd735b..c3088de 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,8 @@ Planned domain namespaces are stubbed for parity tracking: - `gotchi`, `portal`, `wearables`, `items`, `inventory`, `baazaar`, `auction`, `lending`, `staking`, `gotchi-points`, `realm`, `alchemica`, `forge`, `token` Many Base-era write flows are already executable as mapped aliases in those namespaces (internally routed through `onchain send`). -Example: `ag lending create --abi-file ./abis/GotchiLendingFacet.json --address 0x... --args-json '[...]' --json` +Example with built-in defaults: `ag auction bid --args-json '[...]' --dry-run --json` +Example with explicit metadata: `ag lending create --abi-file ./abis/GotchiLendingFacet.json --address 0x... --args-json '[...]' --json` ## Command help and discoverability @@ -65,10 +66,11 @@ ag tx send --help ag help baazaar buy-now ``` -Mapped write commands now expose their onchain function mapping and required flags: +Mapped write commands now expose their onchain function mapping, defaults (if available), and required flags: ```bash ag baazaar buy-now --help +ag auction bid --help ``` If you provide `--abi-file` with `--help`, the CLI prints ABI-derived function signature and input names for the mapped method: diff --git a/src/commands/mapped-defaults.ts b/src/commands/mapped-defaults.ts new file mode 100644 index 0000000..22c58e0 --- /dev/null +++ b/src/commands/mapped-defaults.ts @@ -0,0 +1,49 @@ +import { parseAbi, type Abi } from "viem"; + +import { BASE_GBM_DIAMOND } from "../subgraph/sources"; + +export interface MappedWriteDefaults { + address?: `0x${string}`; + abi?: Abi; + source: string; +} + +const GBM_MAPPED_WRITE_ABI = parseAbi([ + "function buyNow(uint256 _auctionID)", + "function cancelAuction(uint256 _auctionID)", + "function commitBid(uint256 _auctionID,uint256 _bidAmount,uint256 _highestBid,address _tokenContract,uint256 _tokenID,uint256 _amount,bytes _unused)", + "function swapAndCommitBid((address tokenIn,uint256 swapAmount,uint256 minGhstOut,uint256 swapDeadline,address recipient,uint256 auctionID,uint256 bidAmount,uint256 highestBid,address tokenContract,uint256 _tokenID,uint256 _amount,bytes _signature) ctx)", + "function createAuction((uint80 startTime,uint80 endTime,uint56 tokenAmount,uint8 category,bytes4 tokenKind,uint256 tokenID,uint96 buyItNowPrice,uint96 startingBid) _info,address _tokenContract,uint256 _auctionPresetID) returns (uint256)", +]); + +const MAPPED_WRITE_DEFAULTS: Record = { + "auction bid": { + address: BASE_GBM_DIAMOND, + abi: GBM_MAPPED_WRITE_ABI, + source: "base.gbm-diamond", + }, + "auction buy-now": { + address: BASE_GBM_DIAMOND, + abi: GBM_MAPPED_WRITE_ABI, + source: "base.gbm-diamond", + }, + "auction cancel": { + address: BASE_GBM_DIAMOND, + abi: GBM_MAPPED_WRITE_ABI, + source: "base.gbm-diamond", + }, + "auction create": { + address: BASE_GBM_DIAMOND, + abi: GBM_MAPPED_WRITE_ABI, + source: "base.gbm-diamond", + }, + "auction swap-bid": { + address: BASE_GBM_DIAMOND, + abi: GBM_MAPPED_WRITE_ABI, + source: "base.gbm-diamond", + }, +}; + +export function getMappedWriteDefaults(commandPath: string[]): MappedWriteDefaults | undefined { + return MAPPED_WRITE_DEFAULTS[commandPath.join(" ")]; +} diff --git a/src/commands/mapped-runtime.test.ts b/src/commands/mapped-runtime.test.ts new file mode 100644 index 0000000..637772e --- /dev/null +++ b/src/commands/mapped-runtime.test.ts @@ -0,0 +1,82 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { CommandContext } from "../types"; +import { BASE_GBM_DIAMOND } from "../subgraph/sources"; + +const { runOnchainSendWithFunctionMock } = vi.hoisted(() => ({ + runOnchainSendWithFunctionMock: vi.fn(), +})); + +vi.mock("./onchain", () => ({ + runOnchainSendWithFunction: runOnchainSendWithFunctionMock, +})); + +import { runMappedDomainCommand } from "./mapped"; + +function createCtx(path: string[]): CommandContext { + return { + commandPath: path, + args: { + positionals: path, + flags: { + "args-json": "[]", + }, + }, + globals: { + mode: "agent", + json: true, + yes: true, + profile: "prod", + }, + }; +} + +describe("mapped command execution defaults", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("injects built-in GBM defaults for auction bid", async () => { + runOnchainSendWithFunctionMock.mockResolvedValue({ ok: true }); + + const result = await runMappedDomainCommand(createCtx(["auction", "bid"])); + + expect(runOnchainSendWithFunctionMock).toHaveBeenCalledTimes(1); + expect(runOnchainSendWithFunctionMock).toHaveBeenCalledWith( + expect.objectContaining({ commandPath: ["auction", "bid"] }), + "commitBid", + "auction bid", + expect.objectContaining({ + address: BASE_GBM_DIAMOND, + source: "base.gbm-diamond", + }), + ); + expect(result).toMatchObject({ + mappedMethod: "commitBid", + defaults: { + source: "base.gbm-diamond", + address: BASE_GBM_DIAMOND, + abi: "available", + }, + result: { ok: true }, + }); + }); + + it("keeps non-defaulted mapped commands requiring explicit metadata", async () => { + runOnchainSendWithFunctionMock.mockResolvedValue({ ok: true }); + + const result = await runMappedDomainCommand(createCtx(["baazaar", "buy-now"])); + const call = runOnchainSendWithFunctionMock.mock.calls[0]; + const defaultsArg = call?.[3] as { abi?: unknown; address?: unknown; source?: unknown }; + + expect(runOnchainSendWithFunctionMock).toHaveBeenCalledTimes(1); + expect(defaultsArg.abi).toBeUndefined(); + expect(defaultsArg.address).toBeUndefined(); + expect(defaultsArg.source).toBeUndefined(); + expect(result).toMatchObject({ + mappedMethod: "buyNow", + defaults: null, + result: { ok: true }, + }); + }); +}); diff --git a/src/commands/mapped.ts b/src/commands/mapped.ts index 37ba082..4ea7aef 100644 --- a/src/commands/mapped.ts +++ b/src/commands/mapped.ts @@ -1,6 +1,7 @@ import { CliError } from "../errors"; import { CommandContext, JsonValue } from "../types"; +import { getMappedWriteDefaults } from "./mapped-defaults"; import { runOnchainSendWithFunction } from "./onchain"; const MAPPED_WRITE_COMMANDS: Record = { @@ -74,10 +75,22 @@ export async function runMappedDomainCommand(ctx: CommandContext): Promise { const config = loadConfig(); const profileName = getFlagString(ctx.args.flags, "profile") || ctx.globals.profile; @@ -136,10 +141,31 @@ export async function runOnchainSendWithFunction( const chain = resolveChain(profile.chain); const rpcUrl = resolveRpcUrl(chain, getFlagString(ctx.args.flags, "rpc-url") || profile.rpcUrl); - const abiFile = requireFlag(getFlagString(ctx.args.flags, "abi-file"), "--abi-file"); - const abi = parseAbiFile(abiFile); - - const address = parseAddress(getFlagString(ctx.args.flags, "address"), "--address"); + const abiFile = getFlagString(ctx.args.flags, "abi-file"); + const abi = abiFile + ? parseAbiFile(abiFile) + : defaults?.abi || + (() => { + throw new CliError("MISSING_ARGUMENT", "--abi-file is required.", 2, { + command: commandOverride || ctx.commandPath.join(" "), + hint: forcedFunctionName + ? "This mapped command has no built-in ABI metadata yet. Provide --abi-file." + : "Provide --abi-file .", + }); + })(); + + const addressInput = getFlagString(ctx.args.flags, "address"); + const address = addressInput + ? parseAddress(addressInput, "--address") + : defaults?.address || + (() => { + throw new CliError("MISSING_ARGUMENT", "--address is required.", 2, { + command: commandOverride || ctx.commandPath.join(" "), + hint: forcedFunctionName + ? "This mapped command has no built-in contract address yet. Provide --address." + : "Provide --address <0x...>.", + }); + })(); const functionName = forcedFunctionName || requireFlag(getFlagString(ctx.args.flags, "function"), "--function"); const args = parseArgsJson(getFlagString(ctx.args.flags, "args-json")); @@ -197,6 +223,11 @@ export async function runOnchainSendWithFunction( address, functionName, args, + defaults: { + abi: abiFile ? "flag" : defaults?.abi ? "mapped-default" : "none", + address: addressInput ? "flag" : defaults?.address ? "mapped-default" : "none", + source: defaults?.source || null, + }, result, }; } diff --git a/src/commands/write-dryrun.test.ts b/src/commands/write-dryrun.test.ts index e5b1093..2c3815f 100644 --- a/src/commands/write-dryrun.test.ts +++ b/src/commands/write-dryrun.test.ts @@ -3,6 +3,7 @@ import * as os from "os"; import * as path from "path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { parseAbi } from "viem"; import { CommandContext } from "../types"; @@ -37,7 +38,7 @@ vi.mock("../tx-engine", () => ({ executeTxIntent: executeTxIntentMock, })); -import { runOnchainSendCommand } from "./onchain"; +import { runOnchainSendCommand, runOnchainSendWithFunction } from "./onchain"; import { runTxSendCommand } from "./tx"; const files: string[] = []; @@ -188,6 +189,42 @@ describe("write command dry-run flags", () => { ); }); + it("accepts mapped defaults when --abi-file and --address are omitted", async () => { + const result = await runOnchainSendWithFunction( + createContext(["auction", "bid"], { + "args-json": '["1","1","1","0x1111111111111111111111111111111111111111","1","1","0x"]', + "dry-run": true, + }), + "commitBid", + "auction bid", + { + abi: parseAbi(["function commitBid(uint256,uint256,uint256,address,uint256,uint256,bytes)"]), + address: "0x80320a0000c7a6a34086e2acad6915ff57ffda31", + source: "base.gbm-diamond", + }, + ); + + expect(executeTxIntentMock).toHaveBeenCalledWith( + expect.objectContaining({ + command: "auction bid", + to: "0x80320a0000c7a6a34086e2acad6915ff57ffda31", + dryRun: true, + }), + expect.objectContaining({ chainId: 8453 }), + ); + expect( + result as { + defaults: { abi: string; address: string; source: string }; + }, + ).toMatchObject({ + defaults: { + abi: "mapped-default", + address: "mapped-default", + source: "base.gbm-diamond", + }, + }); + }); + it("rejects --dry-run with --wait on onchain send", async () => { const abiFile = writeAbiFile( JSON.stringify([ diff --git a/src/output.test.ts b/src/output.test.ts index c292a15..c93953c 100644 --- a/src/output.test.ts +++ b/src/output.test.ts @@ -18,6 +18,16 @@ describe("help output", () => { expect(text).toContain("Mapped to onchain function:"); expect(text).toContain("buyNow"); expect(text).toContain("--args-json"); + expect(text).toContain("--abi-file --address <0x...> --args-json"); + }); + + it("prints built-in mapped defaults for auction bid without --abi-file", () => { + const text = buildHelpText(["auction", "bid"]); + expect(text).toContain("commitBid(uint256,uint256,uint256,address,uint256,uint256,bytes)"); + expect(text).toContain("source: base.gbm-diamond"); + expect(text).toContain("address: 0x80320a0000c7a6a34086e2acad6915ff57ffda31"); + expect(text).toContain("ag auction bid --profile --args-json"); + expect(text).toContain("--abi-file (override built-in ABI)"); }); it("prints ABI-derived mapped function signature when --abi-file is supplied", () => { diff --git a/src/output.ts b/src/output.ts index e8efa7a..584c42d 100644 --- a/src/output.ts +++ b/src/output.ts @@ -6,6 +6,7 @@ import { parseAbiFile, } from "./abi"; import { suggestCommands } from "./command-catalog"; +import { getMappedWriteDefaults } from "./commands/mapped-defaults"; import { findMappedFunction, listMappedCommandsForRoot } from "./commands/mapped"; import { isDomainStubRoot } from "./commands/stubs"; import { CliError } from "./errors"; @@ -337,6 +338,7 @@ function buildMappedCommandHelp(commandPath: string[], flags: Flags): string { return ""; } + const defaults = getMappedWriteDefaults(commandPath); const abiFile = getFlagString(flags, "abi-file"); const signatureLines: string[] = []; const inputLines: string[] = []; @@ -357,25 +359,87 @@ function buildMappedCommandHelp(commandPath: string[], flags: Flags): string { const message = error instanceof Error ? error.message : "Unable to parse ABI file."; signatureLines.push(`Could not inspect ABI file '${abiFile}': ${message}`); } + } else if (defaults?.abi) { + const entries = getAbiFunctionEntries(defaults.abi, method); + if (entries.length === 0) { + signatureLines.push("Built-in ABI defaults are available, but no matching function entry was found."); + } else { + for (const entry of entries) { + signatureLines.push(formatAbiFunctionSignature(entry)); + inputLines.push(...formatAbiFunctionInputs(entry)); + } + } } else { signatureLines.push("Pass --abi-file with --help to print exact ABI-derived signature and input names."); } + const requiredFlags = ["--args-json", "--profile (or active profile)"]; + const optionalOverrides: string[] = []; + + if (!defaults?.abi) { + requiredFlags.unshift("--abi-file"); + } else { + optionalOverrides.push("--abi-file (override built-in ABI)"); + } + + if (!defaults?.address) { + requiredFlags.unshift("--address"); + } else { + optionalOverrides.push("--address (override default)"); + } + + const usageParts = [`ag ${command}`, "--profile "]; + if (!defaults?.abi) { + usageParts.push("--abi-file "); + } + if (!defaults?.address) { + usageParts.push("--address <0x...>"); + } + usageParts.push( + "--args-json '[...]'", + "[--value-wei ]", + "[--nonce-policy ]", + "[--nonce ]", + "[--dry-run]", + "[--wait]", + "[--json]", + ); + const usage = ` ${usageParts.join(" ")}`; + + const defaultLines = [ + defaults?.address ? `address: ${defaults.address}` : "address: none", + defaults?.abi ? "abi: available" : "abi: none", + `source: ${defaults?.source || "none"}`, + ]; + + const dryRunParts = [`ag ${command}`, "--profile prod"]; + if (!defaults?.abi) { + dryRunParts.push("--abi-file ./abi.json"); + } + if (!defaults?.address) { + dryRunParts.push("--address 0xabc..."); + } + dryRunParts.push("--args-json '[,]'", "--dry-run", "--json"); + const dryRunExample = ` ${dryRunParts.join(" ")}`; + return ` Usage: - ag ${command} --profile --abi-file --address <0x...> --args-json '[...]' [--value-wei ] [--nonce-policy ] [--nonce ] [--dry-run] [--wait] [--json] +${usage} Mapped to onchain function: ${method} Required flags: - --abi-file - --address - --args-json - --profile (or active profile) +${toLines(requiredFlags)} + +Mapped defaults: +${toLines(defaultLines)} + +Optional overrides: +${toLines(optionalOverrides)} Dry-run example: - ag ${command} --profile prod --abi-file ./abi.json --address 0xabc... --args-json '[,]' --dry-run --json +${dryRunExample} ABI signature info: ${toLines(signatureLines)}