aavegotchi-cli 0.1.0

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.
@@ -0,0 +1,285 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.executeTxIntent = executeTxIntent;
4
+ exports.getJournalEntryByIdempotency = getJournalEntryByIdempotency;
5
+ exports.getJournalEntryByHash = getJournalEntryByHash;
6
+ exports.getRecentJournalEntries = getRecentJournalEntries;
7
+ exports.resumeTransaction = resumeTransaction;
8
+ const crypto_1 = require("crypto");
9
+ const chains_1 = require("./chains");
10
+ const config_1 = require("./config");
11
+ const errors_1 = require("./errors");
12
+ const idempotency_1 = require("./idempotency");
13
+ const journal_1 = require("./journal");
14
+ const policy_1 = require("./policy");
15
+ const rpc_1 = require("./rpc");
16
+ const signer_1 = require("./signer");
17
+ function toHexData(data) {
18
+ return (data || "0x");
19
+ }
20
+ function formatReceipt(receipt) {
21
+ return {
22
+ blockNumber: receipt.blockNumber.toString(),
23
+ gasUsed: receipt.gasUsed.toString(),
24
+ status: receipt.status,
25
+ };
26
+ }
27
+ function mapJournalToResult(entry) {
28
+ const receipt = entry.receiptJson ? JSON.parse(entry.receiptJson) : undefined;
29
+ return {
30
+ idempotencyKey: entry.idempotencyKey,
31
+ txHash: entry.txHash,
32
+ from: entry.fromAddress,
33
+ to: entry.toAddress,
34
+ nonce: entry.nonce,
35
+ gasLimit: entry.gasLimit,
36
+ maxFeePerGasWei: entry.maxFeePerGasWei || undefined,
37
+ maxPriorityFeePerGasWei: entry.maxPriorityFeePerGasWei || undefined,
38
+ status: entry.status === "confirmed" ? "confirmed" : "submitted",
39
+ receipt,
40
+ };
41
+ }
42
+ async function resolveNonce(intent, ctx, address) {
43
+ if (intent.noncePolicy === "manual") {
44
+ if (intent.nonce === undefined) {
45
+ throw new errors_1.CliError("MISSING_NONCE", "nonce-policy manual requires --nonce.", 2);
46
+ }
47
+ return intent.nonce;
48
+ }
49
+ if (intent.noncePolicy === "replace" && ctx.existing && ctx.existing.nonce >= 0) {
50
+ return ctx.existing.nonce;
51
+ }
52
+ const pendingNonce = await ctx.client.getTransactionCount({
53
+ address,
54
+ blockTag: "pending",
55
+ });
56
+ return pendingNonce;
57
+ }
58
+ async function waitForConfirmation(intent, ctx, txHash) {
59
+ const receipt = await ctx.client.waitForTransactionReceipt({
60
+ hash: txHash,
61
+ timeout: intent.timeoutMs,
62
+ });
63
+ const summary = {
64
+ blockNumber: receipt.blockNumber,
65
+ gasUsed: receipt.gasUsed,
66
+ status: receipt.status,
67
+ };
68
+ const formatted = formatReceipt(summary);
69
+ ctx.journal.markConfirmed(ctx.idempotencyKey, JSON.stringify(formatted));
70
+ return formatted;
71
+ }
72
+ async function executeTxIntent(intent, chain, customHome) {
73
+ const idempotencyKey = (0, idempotency_1.resolveIdempotencyKey)(intent);
74
+ const journal = new journal_1.JournalStore((0, config_1.resolveJournalPath)(customHome));
75
+ try {
76
+ const preflight = await (0, rpc_1.runRpcPreflight)(chain, intent.rpcUrl);
77
+ const existing = journal.getByIdempotencyKey(idempotencyKey);
78
+ if (existing && existing.status === "confirmed") {
79
+ return mapJournalToResult(existing);
80
+ }
81
+ if (existing && existing.status === "submitted" && existing.txHash) {
82
+ if (!intent.waitForReceipt) {
83
+ return mapJournalToResult(existing);
84
+ }
85
+ const receipt = await waitForConfirmation(intent, { client: preflight.client, journal, idempotencyKey, existing }, existing.txHash);
86
+ return {
87
+ ...mapJournalToResult(existing),
88
+ status: "confirmed",
89
+ receipt,
90
+ };
91
+ }
92
+ const viemChain = (0, chains_1.toViemChain)(chain, intent.rpcUrl);
93
+ 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
+ throw new errors_1.CliError("READONLY_SIGNER", "Selected signer cannot submit transactions.", 2, {
96
+ signerType: signerRuntime.summary.signerType,
97
+ backendStatus: signerRuntime.summary.backendStatus,
98
+ });
99
+ }
100
+ const fromAddress = signerRuntime.summary.address;
101
+ const toAddress = intent.to;
102
+ const dataHex = toHexData(intent.data);
103
+ // Preflight simulation catches most runtime reverts before submit.
104
+ try {
105
+ await preflight.client.call({
106
+ account: fromAddress,
107
+ to: toAddress,
108
+ data: dataHex,
109
+ value: intent.valueWei,
110
+ });
111
+ }
112
+ catch (error) {
113
+ throw new errors_1.CliError("SIMULATION_REVERT", "Transaction simulation reverted.", 2, {
114
+ message: error instanceof Error ? error.message : String(error),
115
+ });
116
+ }
117
+ const gasLimit = await preflight.client.estimateGas({
118
+ account: fromAddress,
119
+ to: toAddress,
120
+ data: dataHex,
121
+ value: intent.valueWei,
122
+ });
123
+ const feeEstimate = await preflight.client.estimateFeesPerGas();
124
+ const maxFeePerGas = feeEstimate.maxFeePerGas;
125
+ const maxPriorityFeePerGas = feeEstimate.maxPriorityFeePerGas;
126
+ const balanceWei = await preflight.client.getBalance({ address: fromAddress });
127
+ const requiredWei = (intent.valueWei || 0n) + gasLimit * (maxFeePerGas || 0n);
128
+ if (balanceWei < requiredWei) {
129
+ throw new errors_1.CliError("INSUFFICIENT_FUNDS_PRECHECK", "Account balance is below estimated transaction requirement.", 2, {
130
+ from: fromAddress,
131
+ balanceWei: balanceWei.toString(),
132
+ requiredWei: requiredWei.toString(),
133
+ });
134
+ }
135
+ (0, policy_1.enforcePolicy)({
136
+ policy: intent.policy,
137
+ to: toAddress,
138
+ valueWei: intent.valueWei,
139
+ gasLimit,
140
+ maxFeePerGasWei: maxFeePerGas,
141
+ maxPriorityFeePerGasWei: maxPriorityFeePerGas,
142
+ });
143
+ const ctx = {
144
+ client: preflight.client,
145
+ journal,
146
+ idempotencyKey,
147
+ existing,
148
+ };
149
+ const nonce = await resolveNonce(intent, ctx, fromAddress);
150
+ journal.upsertPrepared({
151
+ idempotencyKey,
152
+ profileName: intent.profileName,
153
+ chainId: intent.chainId,
154
+ command: intent.command,
155
+ toAddress,
156
+ fromAddress,
157
+ valueWei: intent.valueWei?.toString() || "0",
158
+ dataHex,
159
+ nonce,
160
+ gasLimit: gasLimit.toString(),
161
+ maxFeePerGasWei: maxFeePerGas?.toString() || "",
162
+ maxPriorityFeePerGasWei: maxPriorityFeePerGas?.toString() || "",
163
+ status: "prepared",
164
+ });
165
+ const txHash = await signerRuntime.sendTransaction({
166
+ chain: viemChain,
167
+ to: toAddress,
168
+ data: dataHex,
169
+ value: intent.valueWei,
170
+ gas: gasLimit,
171
+ nonce,
172
+ ...(maxFeePerGas ? { maxFeePerGas } : {}),
173
+ ...(maxPriorityFeePerGas ? { maxPriorityFeePerGas } : {}),
174
+ });
175
+ journal.markSubmitted({
176
+ idempotencyKey,
177
+ txHash,
178
+ status: "submitted",
179
+ errorCode: "",
180
+ errorMessage: "",
181
+ });
182
+ if (!intent.waitForReceipt) {
183
+ return {
184
+ idempotencyKey,
185
+ txHash,
186
+ from: fromAddress,
187
+ to: toAddress,
188
+ nonce,
189
+ gasLimit: gasLimit.toString(),
190
+ maxFeePerGasWei: maxFeePerGas?.toString(),
191
+ maxPriorityFeePerGasWei: maxPriorityFeePerGas?.toString(),
192
+ status: "submitted",
193
+ };
194
+ }
195
+ const receipt = await waitForConfirmation(intent, ctx, txHash);
196
+ return {
197
+ idempotencyKey,
198
+ txHash,
199
+ from: fromAddress,
200
+ to: toAddress,
201
+ nonce,
202
+ gasLimit: gasLimit.toString(),
203
+ maxFeePerGasWei: maxFeePerGas?.toString(),
204
+ maxPriorityFeePerGasWei: maxPriorityFeePerGas?.toString(),
205
+ status: "confirmed",
206
+ receipt,
207
+ };
208
+ }
209
+ catch (error) {
210
+ if (error instanceof errors_1.CliError) {
211
+ journal.markFailed(idempotencyKey, error.code, error.message);
212
+ throw error;
213
+ }
214
+ const unknown = new errors_1.CliError("TX_EXECUTION_FAILED", "Transaction execution failed.", 1, {
215
+ correlationId: (0, crypto_1.randomUUID)(),
216
+ message: error instanceof Error ? error.message : String(error),
217
+ });
218
+ journal.markFailed(idempotencyKey, unknown.code, unknown.message);
219
+ throw unknown;
220
+ }
221
+ finally {
222
+ journal.close();
223
+ }
224
+ }
225
+ function getJournalEntryByIdempotency(idempotencyKey, customHome) {
226
+ const journal = new journal_1.JournalStore((0, config_1.resolveJournalPath)(customHome));
227
+ try {
228
+ return journal.getByIdempotencyKey(idempotencyKey);
229
+ }
230
+ finally {
231
+ journal.close();
232
+ }
233
+ }
234
+ function getJournalEntryByHash(txHash, customHome) {
235
+ const journal = new journal_1.JournalStore((0, config_1.resolveJournalPath)(customHome));
236
+ try {
237
+ return journal.getByTxHash(txHash);
238
+ }
239
+ finally {
240
+ journal.close();
241
+ }
242
+ }
243
+ function getRecentJournalEntries(limit = 20, customHome) {
244
+ const journal = new journal_1.JournalStore((0, config_1.resolveJournalPath)(customHome));
245
+ try {
246
+ return journal.listRecent(limit);
247
+ }
248
+ finally {
249
+ journal.close();
250
+ }
251
+ }
252
+ async function resumeTransaction(idempotencyKey, chain, rpcUrl, timeoutMs = 120000, customHome) {
253
+ const journal = new journal_1.JournalStore((0, config_1.resolveJournalPath)(customHome));
254
+ try {
255
+ const entry = journal.getByIdempotencyKey(idempotencyKey);
256
+ if (!entry) {
257
+ throw new errors_1.CliError("TX_NOT_FOUND", `No transaction found for idempotency key '${idempotencyKey}'.`, 2);
258
+ }
259
+ if (!entry.txHash) {
260
+ throw new errors_1.CliError("TX_NOT_FOUND", `Transaction '${idempotencyKey}' has no submitted hash yet.`, 2);
261
+ }
262
+ if (entry.status === "confirmed") {
263
+ return mapJournalToResult(entry);
264
+ }
265
+ const preflight = await (0, rpc_1.runRpcPreflight)(chain, rpcUrl);
266
+ const receipt = await preflight.client.waitForTransactionReceipt({
267
+ hash: entry.txHash,
268
+ timeout: timeoutMs,
269
+ });
270
+ const receiptSummary = formatReceipt({
271
+ blockNumber: receipt.blockNumber,
272
+ gasUsed: receipt.gasUsed,
273
+ status: receipt.status,
274
+ });
275
+ journal.markConfirmed(idempotencyKey, JSON.stringify(receiptSummary));
276
+ return {
277
+ ...mapJournalToResult(entry),
278
+ status: "confirmed",
279
+ receipt: receiptSummary,
280
+ };
281
+ }
282
+ finally {
283
+ journal.close();
284
+ }
285
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "aavegotchi-cli",
3
+ "version": "0.1.0",
4
+ "description": "Agent-first CLI for automating Aavegotchi app and onchain workflows",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/aavegotchi/aavegotchi-cli.git"
9
+ },
10
+ "type": "commonjs",
11
+ "main": "dist/index.js",
12
+ "files": [
13
+ "dist",
14
+ "README.md"
15
+ ],
16
+ "bin": {
17
+ "ag": "dist/index.js",
18
+ "aavegotchi-cli": "dist/index.js"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "engines": {
24
+ "node": ">=22"
25
+ },
26
+ "scripts": {
27
+ "build": "rm -rf dist && tsc -p tsconfig.json",
28
+ "typecheck": "tsc --noEmit",
29
+ "test": "vitest run",
30
+ "test:watch": "vitest",
31
+ "parity:check": "node scripts/check-parity.mjs",
32
+ "ag": "tsx src/index.ts",
33
+ "prepack": "npm run build",
34
+ "bootstrap:smoke": "AGCLI_HOME=/tmp/agcli-smoke tsx src/index.ts bootstrap --mode agent --profile smoke --chain base --signer readonly --json"
35
+ },
36
+ "dependencies": {
37
+ "better-sqlite3": "^11.8.1",
38
+ "pino": "^9.11.0",
39
+ "viem": "^2.39.3",
40
+ "yaml": "^2.8.1",
41
+ "zod": "^3.25.76"
42
+ },
43
+ "devDependencies": {
44
+ "@types/better-sqlite3": "^7.6.13",
45
+ "@types/node": "^22.13.10",
46
+ "tsx": "^4.20.5",
47
+ "typescript": "^5.8.2",
48
+ "vitest": "^4.0.18"
49
+ }
50
+ }