aavegotchi-cli 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1 - 2026-02-27
4
+
5
+ ### Added
6
+
7
+ - `--dry-run` mode for write surfaces:
8
+ - `ag tx send --dry-run`
9
+ - `ag onchain send --dry-run`
10
+ - mapped writes (for example `ag token approve --dry-run`)
11
+ - `scripts/smoke-write-dryrun.sh` and npm script `smoke:write-dryrun` for automated write-path smoke checks without broadcasting.
12
+
13
+ ### Changed
14
+
15
+ - Dry-run execution now returns `status: "simulated"` with simulation details and skips journal mutation and transaction submission.
16
+ - `--dry-run` is explicitly blocked with `--wait` / `--confirm`.
17
+
3
18
  ## 0.2.0 - 2026-02-27
4
19
 
5
20
  ### Added
package/README.md CHANGED
@@ -55,6 +55,26 @@ Planned domain namespaces are stubbed for parity tracking:
55
55
  Many Base-era write flows are already executable as mapped aliases in those namespaces (internally routed through `onchain send`).
56
56
  Example: `ag lending create --abi-file ./abis/GotchiLendingFacet.json --address 0x... --args-json '[...]' --json`
57
57
 
58
+ ## Dry-run writes
59
+
60
+ Use `--dry-run` on write commands to run full preflight without broadcasting:
61
+
62
+ - runs simulation (`eth_call`)
63
+ - runs gas + fee estimation
64
+ - enforces policy checks
65
+ - resolves nonce
66
+ - returns `status: \"simulated\"` with simulation details
67
+
68
+ Supported write surfaces:
69
+
70
+ - `tx send --dry-run`
71
+ - `onchain send --dry-run`
72
+ - mapped write aliases (for example: `token approve --dry-run`)
73
+
74
+ Safety rule:
75
+
76
+ - `--dry-run` cannot be combined with `--wait` / `--confirm`
77
+
58
78
  ## Subgraph sources and endpoint policy
59
79
 
60
80
  Canonical source aliases:
@@ -186,5 +206,12 @@ npm run typecheck
186
206
  npm test
187
207
  npm run build
188
208
  npm run parity:check
209
+ npm run smoke:write-dryrun
189
210
  npm run ag -- help
190
211
  ```
212
+
213
+ Write dry-run smoke test notes:
214
+
215
+ - `npm run smoke:write-dryrun` validates write paths without broadcasting any transaction.
216
+ - To run against an installed binary instead of local source:
217
+ - `AG_BIN=/absolute/path/to/ag npm run smoke:write-dryrun`
@@ -184,6 +184,10 @@ async function runOnchainSendWithFunction(ctx, forcedFunctionName, commandOverri
184
184
  });
185
185
  }
186
186
  const waitForReceipt = (0, args_1.getFlagBoolean)(ctx.args.flags, "wait");
187
+ const dryRun = (0, args_1.getFlagBoolean)(ctx.args.flags, "dry-run");
188
+ if (dryRun && waitForReceipt) {
189
+ throw new errors_1.CliError("INVALID_ARGUMENT", "--dry-run cannot be combined with --wait.", 2);
190
+ }
187
191
  const intent = {
188
192
  idempotencyKey: (0, args_1.getFlagString)(ctx.args.flags, "idempotency-key"),
189
193
  profileName: profile.name,
@@ -197,6 +201,7 @@ async function runOnchainSendWithFunction(ctx, forcedFunctionName, commandOverri
197
201
  noncePolicy: noncePolicyRaw,
198
202
  nonce,
199
203
  waitForReceipt,
204
+ dryRun,
200
205
  timeoutMs: parseTimeoutMs((0, args_1.getFlagString)(ctx.args.flags, "timeout-ms")),
201
206
  command: commandOverride || `onchain send ${functionName}`,
202
207
  };
@@ -75,10 +75,14 @@ async function runTxSendCommand(ctx) {
75
75
  const nonceValue = (0, args_1.getFlagString)(ctx.args.flags, "nonce");
76
76
  const nonce = nonceValue ? parseNumberFlag(nonceValue, "--nonce", 0) : undefined;
77
77
  const waitForReceipt = (0, args_1.getFlagBoolean)(ctx.args.flags, "wait") || (0, args_1.getFlagBoolean)(ctx.args.flags, "confirm");
78
+ const dryRun = (0, args_1.getFlagBoolean)(ctx.args.flags, "dry-run");
78
79
  const timeoutMs = parseNumberFlag((0, args_1.getFlagString)(ctx.args.flags, "timeout-ms"), "--timeout-ms", 120000);
79
80
  if (noncePolicy === "manual" && nonce === undefined) {
80
81
  throw new errors_1.CliError("MISSING_NONCE", "--nonce is required when --nonce-policy=manual.", 2);
81
82
  }
83
+ if (dryRun && waitForReceipt) {
84
+ throw new errors_1.CliError("INVALID_ARGUMENT", "--dry-run cannot be combined with --wait/--confirm.", 2);
85
+ }
82
86
  const intent = {
83
87
  idempotencyKey,
84
88
  profileName: profile.name,
@@ -92,6 +96,7 @@ async function runTxSendCommand(ctx) {
92
96
  noncePolicy,
93
97
  nonce,
94
98
  waitForReceipt,
99
+ dryRun,
95
100
  timeoutMs,
96
101
  command: "tx send",
97
102
  };
package/dist/output.js CHANGED
@@ -69,7 +69,7 @@ Core commands:
69
69
  rpc check Verify RPC connectivity + signer backend health
70
70
 
71
71
  Tx commands:
72
- tx send Send a raw EVM transaction with simulation + policy checks + journaling
72
+ tx send Send a raw EVM transaction (or simulate only with --dry-run)
73
73
  tx status Read tx status by idempotency key/hash or list recent
74
74
  tx resume Resume waiting for a previously submitted tx
75
75
  tx watch Poll journal until tx is confirmed
@@ -79,7 +79,7 @@ Automation commands:
79
79
 
80
80
  Power-user commands:
81
81
  onchain call Call any ABI function from --abi-file
82
- onchain send Send any ABI function as a transaction
82
+ onchain send Send any ABI function as a transaction (or simulate with --dry-run)
83
83
  subgraph list|check|query List/check/query canonical Goldsky subgraphs
84
84
 
85
85
  Domain namespaces:
@@ -112,6 +112,7 @@ Examples:
112
112
  ag bootstrap --mode agent --profile prod --chain base --signer env:AGCLI_PRIVATE_KEY --json
113
113
  AGCLI_KEYCHAIN_PASSPHRASE=... AGCLI_PRIVATE_KEY=0x... ag signer keychain import --account-id bot --private-key-env AGCLI_PRIVATE_KEY --json
114
114
  ag tx send --profile prod --to 0xabc... --value-wei 1000000000000000 --wait --json
115
+ ag tx send --profile prod --to 0xabc... --value-wei 0 --dry-run --json
115
116
  ag subgraph check --source core-base --json
116
117
  ag baazaar listing active --kind erc721 --first 20 --json
117
118
  ag auction active --first 20 --json
package/dist/tx-engine.js CHANGED
@@ -72,9 +72,10 @@ async function waitForConfirmation(intent, ctx, txHash) {
72
72
  async function executeTxIntent(intent, chain, customHome) {
73
73
  const idempotencyKey = (0, idempotency_1.resolveIdempotencyKey)(intent);
74
74
  const journal = new journal_1.JournalStore((0, config_1.resolveJournalPath)(customHome));
75
+ const dryRun = Boolean(intent.dryRun);
75
76
  try {
76
77
  const preflight = await (0, rpc_1.runRpcPreflight)(chain, intent.rpcUrl);
77
- const existing = journal.getByIdempotencyKey(idempotencyKey);
78
+ const existing = dryRun ? undefined : journal.getByIdempotencyKey(idempotencyKey);
78
79
  if (existing && existing.status === "confirmed") {
79
80
  return mapJournalToResult(existing);
80
81
  }
@@ -91,7 +92,13 @@ async function executeTxIntent(intent, chain, customHome) {
91
92
  }
92
93
  const viemChain = (0, chains_1.toViemChain)(chain, intent.rpcUrl);
93
94
  const signerRuntime = await (0, signer_1.resolveSignerRuntime)(intent.signer, preflight.client, intent.rpcUrl, viemChain, customHome);
94
- if (!signerRuntime.summary.canSign || !signerRuntime.sendTransaction || !signerRuntime.summary.address) {
95
+ if (!signerRuntime.summary.address) {
96
+ throw new errors_1.CliError("MISSING_SIGNER_ADDRESS", "Signer address is required for simulation and transaction execution.", 2, {
97
+ signerType: signerRuntime.summary.signerType,
98
+ backendStatus: signerRuntime.summary.backendStatus,
99
+ });
100
+ }
101
+ if (!dryRun && (!signerRuntime.summary.canSign || !signerRuntime.sendTransaction)) {
95
102
  throw new errors_1.CliError("READONLY_SIGNER", "Selected signer cannot submit transactions.", 2, {
96
103
  signerType: signerRuntime.summary.signerType,
97
104
  backendStatus: signerRuntime.summary.backendStatus,
@@ -125,7 +132,7 @@ async function executeTxIntent(intent, chain, customHome) {
125
132
  const maxPriorityFeePerGas = feeEstimate.maxPriorityFeePerGas;
126
133
  const balanceWei = await preflight.client.getBalance({ address: fromAddress });
127
134
  const requiredWei = (intent.valueWei || 0n) + gasLimit * (maxFeePerGas || 0n);
128
- if (balanceWei < requiredWei) {
135
+ if (!dryRun && balanceWei < requiredWei) {
129
136
  throw new errors_1.CliError("INSUFFICIENT_FUNDS_PRECHECK", "Account balance is below estimated transaction requirement.", 2, {
130
137
  from: fromAddress,
131
138
  balanceWei: balanceWei.toString(),
@@ -147,6 +154,31 @@ async function executeTxIntent(intent, chain, customHome) {
147
154
  existing,
148
155
  };
149
156
  const nonce = await resolveNonce(intent, ctx, fromAddress);
157
+ if (dryRun) {
158
+ return {
159
+ idempotencyKey,
160
+ from: fromAddress,
161
+ to: toAddress,
162
+ nonce,
163
+ gasLimit: gasLimit.toString(),
164
+ maxFeePerGasWei: maxFeePerGas?.toString(),
165
+ maxPriorityFeePerGasWei: maxPriorityFeePerGas?.toString(),
166
+ status: "simulated",
167
+ dryRun: true,
168
+ simulation: {
169
+ requiredWei: requiredWei.toString(),
170
+ balanceWei: balanceWei.toString(),
171
+ signerCanSign: signerRuntime.summary.canSign,
172
+ noncePolicy: intent.noncePolicy,
173
+ },
174
+ };
175
+ }
176
+ if (!signerRuntime.sendTransaction) {
177
+ throw new errors_1.CliError("READONLY_SIGNER", "Selected signer cannot submit transactions.", 2, {
178
+ signerType: signerRuntime.summary.signerType,
179
+ backendStatus: signerRuntime.summary.backendStatus,
180
+ });
181
+ }
150
182
  journal.upsertPrepared({
151
183
  idempotencyKey,
152
184
  profileName: intent.profileName,
@@ -208,14 +240,18 @@ async function executeTxIntent(intent, chain, customHome) {
208
240
  }
209
241
  catch (error) {
210
242
  if (error instanceof errors_1.CliError) {
211
- journal.markFailed(idempotencyKey, error.code, error.message);
243
+ if (!dryRun) {
244
+ journal.markFailed(idempotencyKey, error.code, error.message);
245
+ }
212
246
  throw error;
213
247
  }
214
248
  const unknown = new errors_1.CliError("TX_EXECUTION_FAILED", "Transaction execution failed.", 1, {
215
249
  correlationId: (0, crypto_1.randomUUID)(),
216
250
  message: error instanceof Error ? error.message : String(error),
217
251
  });
218
- journal.markFailed(idempotencyKey, unknown.code, unknown.message);
252
+ if (!dryRun) {
253
+ journal.markFailed(idempotencyKey, unknown.code, unknown.message);
254
+ }
219
255
  throw unknown;
220
256
  }
221
257
  finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aavegotchi-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Agent-first CLI for automating Aavegotchi app and onchain workflows",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,6 +30,7 @@
30
30
  "test": "vitest run",
31
31
  "test:watch": "vitest",
32
32
  "parity:check": "node scripts/check-parity.mjs",
33
+ "smoke:write-dryrun": "bash scripts/smoke-write-dryrun.sh",
33
34
  "ag": "tsx src/index.ts",
34
35
  "prepack": "npm run build",
35
36
  "bootstrap:smoke": "AGCLI_HOME=/tmp/agcli-smoke tsx src/index.ts bootstrap --mode agent --profile smoke --chain base --signer readonly --json"