aavegotchi-cli 0.2.0 → 0.2.2

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/dist/signer.js CHANGED
@@ -9,6 +9,9 @@ const errors_1 = require("./errors");
9
9
  const keychain_1 = require("./keychain");
10
10
  const REMOTE_SIGN_ADDRESS_PATH = "/address";
11
11
  const REMOTE_SIGN_TX_PATH = "/sign-transaction";
12
+ const BANKR_DEFAULT_API_URL = "https://api.bankr.bot";
13
+ const BANKR_AGENT_ME_PATH = "/agent/me";
14
+ const BANKR_AGENT_SUBMIT_PATH = "/agent/submit";
12
15
  function parsePrivateKey(value, hint) {
13
16
  if (!/^0x[0-9a-fA-F]{64}$/.test(value)) {
14
17
  throw new errors_1.CliError("INVALID_PRIVATE_KEY", `${hint} is not a valid private key.`, 2);
@@ -77,6 +80,111 @@ function buildRemoteHeaders(signer) {
77
80
  }
78
81
  return headers;
79
82
  }
83
+ function requireEnvVarName(value, context) {
84
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(value)) {
85
+ throw new errors_1.CliError("INVALID_SIGNER_SPEC", `Invalid ${context} env var '${value}'.`, 2);
86
+ }
87
+ return value;
88
+ }
89
+ function resolveBankrApiUrl(signer) {
90
+ return (signer.apiUrl || BANKR_DEFAULT_API_URL).replace(/\/+$/, "");
91
+ }
92
+ function buildBankrHeaders(signer) {
93
+ const envVar = signer.apiKeyEnvVar || "BANKR_API_KEY";
94
+ const token = process.env[envVar];
95
+ if (!token) {
96
+ throw new errors_1.CliError("MISSING_SIGNER_SECRET", `Missing environment variable '${envVar}'.`, 2);
97
+ }
98
+ return {
99
+ "content-type": "application/json",
100
+ "x-api-key": token,
101
+ };
102
+ }
103
+ async function fetchBankrJson(url, init) {
104
+ let response;
105
+ try {
106
+ response = await fetch(url, init);
107
+ }
108
+ catch (error) {
109
+ throw new errors_1.CliError("BANKR_API_UNREACHABLE", "Failed to connect to Bankr API.", 2, {
110
+ url,
111
+ message: error instanceof Error ? error.message : String(error),
112
+ });
113
+ }
114
+ const text = await response.text();
115
+ let parsed = {};
116
+ if (text) {
117
+ try {
118
+ parsed = JSON.parse(text);
119
+ }
120
+ catch {
121
+ parsed = text;
122
+ }
123
+ }
124
+ if (!response.ok) {
125
+ throw new errors_1.CliError("BANKR_API_HTTP_ERROR", `Bankr API responded with HTTP ${response.status}.`, 2, {
126
+ url,
127
+ status: response.status,
128
+ body: parsed,
129
+ });
130
+ }
131
+ return parsed;
132
+ }
133
+ function parseBankrAddressCandidate(candidate) {
134
+ if (typeof candidate !== "string") {
135
+ return undefined;
136
+ }
137
+ if (!/^0x[a-fA-F0-9]{40}$/.test(candidate)) {
138
+ return undefined;
139
+ }
140
+ return candidate.toLowerCase();
141
+ }
142
+ async function resolveBankrAddress(signer, headers) {
143
+ if (signer.address) {
144
+ return ensureAddress(signer.address, "bankr signer address");
145
+ }
146
+ const apiUrl = resolveBankrApiUrl(signer);
147
+ const response = (await fetchBankrJson(addDefaultPaths(apiUrl, BANKR_AGENT_ME_PATH), {
148
+ method: "GET",
149
+ headers,
150
+ }));
151
+ const direct = parseBankrAddressCandidate(response.walletAddress) ||
152
+ parseBankrAddressCandidate(response.address) ||
153
+ parseBankrAddressCandidate(response.agent?.walletAddress) ||
154
+ parseBankrAddressCandidate(response.agent?.address);
155
+ if (direct) {
156
+ return direct;
157
+ }
158
+ if (Array.isArray(response.wallets)) {
159
+ const evmWallet = response.wallets.find((wallet) => wallet.chain === "evm");
160
+ const evmAddress = parseBankrAddressCandidate(evmWallet?.address) || parseBankrAddressCandidate(evmWallet?.walletAddress);
161
+ if (evmAddress) {
162
+ return evmAddress;
163
+ }
164
+ for (const wallet of response.wallets) {
165
+ const anyAddress = parseBankrAddressCandidate(wallet.address) || parseBankrAddressCandidate(wallet.walletAddress);
166
+ if (anyAddress) {
167
+ return anyAddress;
168
+ }
169
+ }
170
+ }
171
+ throw new errors_1.CliError("BANKR_API_PROTOCOL_ERROR", "Bankr /agent/me response missing wallet address.", 2, {
172
+ keys: Object.keys(response),
173
+ });
174
+ }
175
+ function parseBankrSubmitHash(response) {
176
+ if (typeof response !== "object" || response === null) {
177
+ return undefined;
178
+ }
179
+ const root = response;
180
+ const nested = typeof root.result === "object" && root.result !== null ? root.result : undefined;
181
+ return (parseTxHash(root.transactionHash) ||
182
+ parseTxHash(root.txHash) ||
183
+ parseTxHash(root.hash) ||
184
+ parseTxHash(nested?.transactionHash) ||
185
+ parseTxHash(nested?.txHash) ||
186
+ parseTxHash(nested?.hash));
187
+ }
80
188
  async function resolveRemoteAddress(signer, headers) {
81
189
  if (signer.address) {
82
190
  return ensureAddress(signer.address, "remote signer address");
@@ -175,6 +283,47 @@ function parseSignerLedgerSpec(value) {
175
283
  }
176
284
  return signer;
177
285
  }
286
+ function parseSignerBankrSpec(value) {
287
+ if (value === "bankr") {
288
+ return { type: "bankr" };
289
+ }
290
+ const body = value.slice("bankr:".length).trim();
291
+ const signer = {
292
+ type: "bankr",
293
+ };
294
+ if (!body) {
295
+ return signer;
296
+ }
297
+ if (!body.includes("|")) {
298
+ if (/^0x[a-fA-F0-9]{40}$/.test(body)) {
299
+ signer.address = ensureAddress(body, "bankr signer address");
300
+ return signer;
301
+ }
302
+ if (/^[A-Z_][A-Z0-9_]*$/.test(body)) {
303
+ signer.apiKeyEnvVar = requireEnvVarName(body, "bankr api key");
304
+ return signer;
305
+ }
306
+ if (/^https?:\/\//i.test(body)) {
307
+ signer.apiUrl = body;
308
+ return signer;
309
+ }
310
+ throw new errors_1.CliError("INVALID_SIGNER_SPEC", `Invalid bankr signer format '${value}'.`, 2);
311
+ }
312
+ const [addressPart, apiKeyEnvVarPart, apiUrlPart] = body.split("|").map((entry) => entry.trim());
313
+ if (addressPart) {
314
+ signer.address = ensureAddress(addressPart, "bankr signer address");
315
+ }
316
+ if (apiKeyEnvVarPart) {
317
+ signer.apiKeyEnvVar = requireEnvVarName(apiKeyEnvVarPart, "bankr api key");
318
+ }
319
+ if (apiUrlPart) {
320
+ if (!/^https?:\/\//i.test(apiUrlPart)) {
321
+ throw new errors_1.CliError("INVALID_SIGNER_SPEC", `Invalid bankr API URL in '${value}'.`, 2);
322
+ }
323
+ signer.apiUrl = apiUrlPart;
324
+ }
325
+ return signer;
326
+ }
178
327
  function parseSigner(value) {
179
328
  if (!value || value === "readonly") {
180
329
  return { type: "readonly" };
@@ -205,7 +354,10 @@ function parseSigner(value) {
205
354
  if (value.startsWith("remote:")) {
206
355
  return parseSignerRemoteSpec(value);
207
356
  }
208
- throw new errors_1.CliError("INVALID_SIGNER_SPEC", `Unsupported signer '${value}'. Use readonly, env:<ENV_VAR>, keychain:<id>, ledger[:path|address|bridgeEnv], or remote:<url|address|authEnv>.`, 2);
357
+ if (value === "bankr" || value.startsWith("bankr:")) {
358
+ return parseSignerBankrSpec(value);
359
+ }
360
+ throw new errors_1.CliError("INVALID_SIGNER_SPEC", `Unsupported signer '${value}'. Use readonly, env:<ENV_VAR>, keychain:<id>, ledger[:path|address|bridgeEnv], remote:<url|address|authEnv>, or bankr[:address|apiKeyEnv|apiUrl].`, 2);
209
361
  }
210
362
  async function resolveSignerRuntime(signer, publicClient, rpcUrl, chain, customHome) {
211
363
  if (signer.type === "readonly") {
@@ -307,6 +459,44 @@ async function resolveSignerRuntime(signer, publicClient, rpcUrl, chain, customH
307
459
  },
308
460
  };
309
461
  }
462
+ if (signer.type === "bankr") {
463
+ const apiUrl = resolveBankrApiUrl(signer);
464
+ const headers = buildBankrHeaders(signer);
465
+ const address = await resolveBankrAddress(signer, headers);
466
+ const summary = await resolveBalanceSummary("bankr", publicClient, address);
467
+ return {
468
+ summary,
469
+ sendTransaction: async (request) => {
470
+ const payload = {
471
+ transaction: {
472
+ chainId: request.chain.id,
473
+ from: address,
474
+ to: request.to,
475
+ data: request.data,
476
+ value: request.value?.toString() || "0",
477
+ gas: request.gas.toString(),
478
+ nonce: request.nonce,
479
+ maxFeePerGas: request.maxFeePerGas?.toString(),
480
+ maxPriorityFeePerGas: request.maxPriorityFeePerGas?.toString(),
481
+ },
482
+ waitForConfirmation: false,
483
+ description: "Submitted via aavegotchi-cli",
484
+ };
485
+ const response = await fetchBankrJson(addDefaultPaths(apiUrl, BANKR_AGENT_SUBMIT_PATH), {
486
+ method: "POST",
487
+ headers,
488
+ body: JSON.stringify(payload),
489
+ });
490
+ const txHash = parseBankrSubmitHash(response);
491
+ if (txHash) {
492
+ return txHash;
493
+ }
494
+ throw new errors_1.CliError("BANKR_API_PROTOCOL_ERROR", "Bankr submit response missing transaction hash.", 2, {
495
+ response,
496
+ });
497
+ },
498
+ };
499
+ }
310
500
  const bridgeEnvVar = signer.bridgeCommandEnvVar || "AGCLI_LEDGER_BRIDGE_CMD";
311
501
  const bridgeCommand = process.env[bridgeEnvVar];
312
502
  if (!bridgeCommand) {
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.2",
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"