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
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Changelog

## 0.2.0 - 2026-02-27

### Added

- Generic subgraph command family:
- `ag subgraph list`
- `ag subgraph check --source core-base|gbm-base [--raw]`
- `ag subgraph query --source <alias> ...`
- Baazaar subgraph wrappers:
- `ag baazaar listing get --kind erc721|erc1155 --id <listingId> [--verify-onchain]`
- `ag baazaar listing active --kind erc721|erc1155 [--first] [--skip]`
- `ag baazaar listing mine --kind erc721|erc1155 --seller <0x...> [--first] [--skip]`
- GBM subgraph wrappers:
- `ag auction get --id <auctionId> [--verify-onchain]`
- `ag auction active [--first] [--skip] [--at-time <unix>]`
- `ag auction mine --seller <0x...> [--first] [--skip]`
- `ag auction bids --auction-id <id> [--first] [--skip]`
- `ag auction bids-mine --bidder <0x...> [--first] [--skip]`
- Optional `--raw` GraphQL payload passthrough while preserving typed projections.
- New docs under `docs/subgraph/` for endpoint policy and query matrix.

### Security and policy

- Strict endpoint allowlist by default for canonical sources only.
- Custom endpoint override requires both `--subgraph-url` and `--allow-untrusted-subgraph`.
- Non-HTTPS custom subgraph endpoints are rejected.

### Notes

- Existing `onchain`, `tx`, mapped write aliases, and `baazaar read` onchain call behavior are preserved.
72 changes: 71 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ For read-only automation:
npm run ag -- bootstrap --mode agent --profile prod --chain base --signer readonly --json
```

## Command surface (current)
## Command surface (v0.2.0)

- `bootstrap`
- `profile list|show|use|export`
Expand All @@ -43,6 +43,9 @@ npm run ag -- bootstrap --mode agent --profile prod --chain base --signer readon
- `tx send|status|resume|watch`
- `batch run --file plan.yaml`
- `onchain call|send`
- `subgraph list|check|query`
- `baazaar listing get|active|mine` (subgraph-first read wrappers)
- `auction get|active|mine|bids|bids-mine` (subgraph-first read wrappers)
- `<domain> read` (routes to generic onchain call for that domain)

Planned domain namespaces are stubbed for parity tracking:
Expand All @@ -52,6 +55,71 @@ Planned domain namespaces are stubbed for parity tracking:
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`

## Subgraph sources and endpoint policy

Canonical source aliases:

- `core-base` -> `https://api.goldsky.com/api/public/project_cmh3flagm0001r4p25foufjtt/subgraphs/aavegotchi-core-base/prod/gn`
- `gbm-base` -> `https://api.goldsky.com/api/public/project_cmh3flagm0001r4p25foufjtt/subgraphs/aavegotchi-gbm-baazaar-base/prod/gn`

Default policy is strict allowlist:

- Non-canonical subgraph URLs are blocked by default (`SUBGRAPH_ENDPOINT_BLOCKED`)
- Override is explicit and per-command only: pass both `--subgraph-url <https-url>` and `--allow-untrusted-subgraph`
- Non-HTTPS custom URLs are rejected

Auth:

- Public Goldsky endpoints work without auth
- If `GOLDSKY_API_KEY` is set, CLI injects `Authorization: Bearer <token>`
- Override env var name per command with `--auth-env-var <ENV>`

## Subgraph command examples

List configured canonical sources:

```bash
npm run ag -- subgraph list --json
```

Check source reachability/introspection:

```bash
npm run ag -- subgraph check --source core-base --json
```

Run custom GraphQL query:

```bash
npm run ag -- subgraph query \
--source gbm-base \
--query 'query($first:Int!){ auctions(first:$first){ id } }' \
--variables-json '{"first":5}' \
--json
```

Baazaar wrappers:

```bash
npm run ag -- baazaar listing active --kind erc721 --first 20 --skip 0 --json
npm run ag -- baazaar listing mine --kind erc1155 --seller 0x... --json
npm run ag -- baazaar listing get --kind erc721 --id 123 --verify-onchain --json
```

GBM wrappers:

```bash
npm run ag -- auction active --first 20 --json
npm run ag -- auction bids --auction-id 123 --json
npm run ag -- auction get --id 123 --verify-onchain --json
```

Raw GraphQL passthrough (typed projection remains included):

```bash
npm run ag -- auction active --first 5 --raw --json
```

## Signer backends

- `readonly` (read-only mode)
Expand Down Expand Up @@ -106,6 +174,8 @@ All successful/error responses use a stable envelope:

- Method inventory: [`docs/parity/base-method-inventory.md`](docs/parity/base-method-inventory.md)
- Command mapping: [`docs/parity/base-command-matrix.md`](docs/parity/base-command-matrix.md)
- Subgraph endpoints/policy: [`docs/subgraph/endpoints-and-policy.md`](docs/subgraph/endpoints-and-policy.md)
- Subgraph query matrix: [`docs/subgraph/query-matrix.md`](docs/subgraph/query-matrix.md)

Raffle/ticket flows are intentionally excluded for Base-era scope.

Expand Down
41 changes: 41 additions & 0 deletions docs/subgraph/endpoints-and-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Subgraph Endpoints and Policy

## Canonical aliases

- `core-base`
- `https://api.goldsky.com/api/public/project_cmh3flagm0001r4p25foufjtt/subgraphs/aavegotchi-core-base/prod/gn`
- `gbm-base`
- `https://api.goldsky.com/api/public/project_cmh3flagm0001r4p25foufjtt/subgraphs/aavegotchi-gbm-baazaar-base/prod/gn`

## Security model (v0.2.0)

- Default: strict allowlist for canonical endpoints only.
- Custom endpoint requires both:
- `--subgraph-url <https-url>`
- `--allow-untrusted-subgraph`
- Custom non-HTTPS endpoint is rejected.

## Auth behavior

- Endpoint works without auth by default.
- CLI injects bearer auth only when env var exists.
- Default env var: `GOLDSKY_API_KEY`
- Per-command override: `--auth-env-var <ENV>`

## Core flags

- `--timeout-ms <ms>`
- `--raw`
- `--subgraph-url <url> --allow-untrusted-subgraph`
- `--auth-env-var <ENV>`

## Error codes

- `SUBGRAPH_SOURCE_UNKNOWN`
- `SUBGRAPH_ENDPOINT_BLOCKED`
- `SUBGRAPH_TIMEOUT`
- `SUBGRAPH_HTTP_ERROR`
- `SUBGRAPH_GRAPHQL_ERROR`
- `SUBGRAPH_INVALID_RESPONSE`
- `SUBGRAPH_VERIFY_MISMATCH`
- `INVALID_VARIABLES_JSON`
78 changes: 78 additions & 0 deletions docs/subgraph/query-matrix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Subgraph Query Matrix

## Generic commands

- `ag subgraph list`
- No network call. Returns canonical alias definitions.

- `ag subgraph check --source core-base|gbm-base [--raw]`
- Runs introspection query.
- Output includes sorted query field names.

- `ag subgraph query --source <alias> (--query <graphql> | --query-file <path>) [--variables-json <json>] [--raw] [--timeout-ms <ms>] [--auth-env-var <ENV>] [--subgraph-url <url> --allow-untrusted-subgraph]`

## Baazaar wrappers (`core-base`)

- `ag baazaar listing get --kind erc721 --id <listingId> [--verify-onchain] [--raw]`
- Query: `erc721Listing(id: $id)`
- Verify path: compares to `getERC721Listing` on Base Aavegotchi diamond.

- `ag baazaar listing get --kind erc1155 --id <listingId> [--verify-onchain] [--raw]`
- Query: `erc1155Listing(id: $id)`
- Verify path: compares to `getERC1155Listing` on Base Aavegotchi diamond.

- `ag baazaar listing active --kind erc721 [--first <n>] [--skip <n>] [--raw]`
- Filter: `{ cancelled: false, timePurchased: "0" }`

- `ag baazaar listing active --kind erc1155 [--first <n>] [--skip <n>] [--raw]`
- Filter: `{ cancelled: false, sold: false }`

- `ag baazaar listing mine --kind erc721 --seller <0x...> [--first <n>] [--skip <n>] [--raw]`
- Filter: `{ seller: $seller }` (`Bytes`, lowercase)

- `ag baazaar listing mine --kind erc1155 --seller <0x...> [--first <n>] [--skip <n>] [--raw]`
- Filter: `{ seller: $seller }` (`Bytes`, lowercase)

## GBM wrappers (`gbm-base`)

- `ag auction get --id <auctionId> [--verify-onchain] [--raw]`
- Query: `auction(id: $id)`
- Verify path compares to `getAuctionHighestBid`, `getContractAddress`, `getTokenId`, `getAuctionStartTime`, `getAuctionEndTime`.

- `ag auction active [--first <n>] [--skip <n>] [--at-time <unix>] [--raw]`
- Filter: `{ claimed: false, cancelled: false, startsAt_lte: $now, endsAt_gt: $now }`

- `ag auction mine --seller <0x...> [--first <n>] [--skip <n>] [--raw]`
- Filter: `{ seller: $seller }` (`Bytes`, lowercase)

- `ag auction bids --auction-id <id> [--first <n>] [--skip <n>] [--raw]`
- Filter: `{ auction: $auctionId }`

- `ag auction bids-mine --bidder <0x...> [--first <n>] [--skip <n>] [--raw]`
- Filter: `{ bidder: $bidder }` (`Bytes`, lowercase)

## Pagination

- `--first` default `20`, min `1`, max `200`
- `--skip` default `0`, min `0`, max `100000`
- No auto-pagination in v0.2.0

## Output contract

All commands return envelope:

- `schemaVersion`
- `command`
- `status`
- `data`
- `meta`

Typed response payload includes:

- `source`
- `endpoint`
- `queryName`
- `pagination` where applicable
- normalized entity fields

`--raw` adds `raw` with complete GraphQL payload while preserving typed projection.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "aavegotchi-cli",
"version": "0.1.0",
"version": "0.2.0",
"description": "Agent-first CLI for automating Aavegotchi app and onchain workflows",
"license": "MIT",
"repository": {
Expand All @@ -11,7 +11,8 @@
"main": "dist/index.js",
"files": [
"dist",
"README.md"
"README.md",
"CHANGELOG.md"
],
"bin": {
"ag": "dist/index.js",
Expand Down
78 changes: 78 additions & 0 deletions src/command-runner.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { afterEach, describe, expect, it, vi } from "vitest";

import { CommandContext } from "./types";

const {
findMappedFunctionMock,
runMappedDomainCommandMock,
runAuctionSubgraphCommandMock,
runBaazaarListingSubgraphCommandMock,
} = vi.hoisted(() => ({
findMappedFunctionMock: vi.fn(),
runMappedDomainCommandMock: vi.fn(),
runAuctionSubgraphCommandMock: vi.fn(),
runBaazaarListingSubgraphCommandMock: vi.fn(),
}));

vi.mock("./commands/mapped", () => ({
findMappedFunction: findMappedFunctionMock,
runMappedDomainCommand: runMappedDomainCommandMock,
}));

vi.mock("./commands/auction-subgraph", () => ({
runAuctionSubgraphCommand: runAuctionSubgraphCommandMock,
}));

vi.mock("./commands/baazaar-subgraph", () => ({
runBaazaarListingSubgraphCommand: runBaazaarListingSubgraphCommandMock,
}));

import { executeCommand } from "./command-runner";

function createCtx(path: string[]): CommandContext {
return {
commandPath: path,
args: { positionals: path, flags: {} },
globals: { mode: "agent", json: true, yes: true },
};
}

describe("command runner routing", () => {
afterEach(() => {
vi.clearAllMocks();
});

it("routes auction active to subgraph wrapper before mapped fallback", async () => {
findMappedFunctionMock.mockReturnValue("unexpectedMapping");
runAuctionSubgraphCommandMock.mockResolvedValue({ auctions: [] });

const result = await executeCommand(createCtx(["auction", "active"]));

expect(result.commandName).toBe("auction active");
expect(result.data).toEqual({ auctions: [] });
expect(runAuctionSubgraphCommandMock).toHaveBeenCalledTimes(1);
expect(runMappedDomainCommandMock).not.toHaveBeenCalled();
});

it("routes baazaar listing get to subgraph wrapper", async () => {
runBaazaarListingSubgraphCommandMock.mockResolvedValue({ listing: null });

const result = await executeCommand(createCtx(["baazaar", "listing", "get"]));

expect(result.commandName).toBe("baazaar listing get");
expect(result.data).toEqual({ listing: null });
expect(runBaazaarListingSubgraphCommandMock).toHaveBeenCalledTimes(1);
});

it("keeps mapped writes working for auction buy-now", async () => {
findMappedFunctionMock.mockReturnValue("buyNow");
runMappedDomainCommandMock.mockResolvedValue({ mappedMethod: "buyNow" });

const result = await executeCommand(createCtx(["auction", "buy-now"]));

expect(result.commandName).toBe("auction buy-now");
expect(result.data).toEqual({ mappedMethod: "buyNow" });
expect(runAuctionSubgraphCommandMock).not.toHaveBeenCalled();
expect(runMappedDomainCommandMock).toHaveBeenCalledTimes(1);
});
});
Loading