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/CHANGELOG.md +43 -0
- package/README.md +69 -0
- package/dist/abi.js +88 -0
- package/dist/command-catalog.js +104 -0
- package/dist/command-runner.js +12 -2
- package/dist/commands/bootstrap.js +9 -0
- package/dist/commands/mapped.js +9 -1
- package/dist/commands/onchain.js +8 -55
- package/dist/commands/stubs.js +9 -1
- package/dist/commands/tx.js +5 -0
- package/dist/index.js +6 -1
- package/dist/output.js +399 -65
- package/dist/schemas.js +6 -0
- package/dist/signer.js +191 -1
- package/dist/tx-engine.js +41 -5
- package/package.json +2 -1
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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"
|