@agtnames/cli 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +68 -2
- package/dist/abi.d.ts +239 -0
- package/dist/abi.js +32 -0
- package/dist/args.d.ts +15 -0
- package/dist/args.js +66 -0
- package/dist/browser.d.ts +18 -0
- package/dist/browser.js +309 -0
- package/dist/chain.d.ts +36 -0
- package/dist/chain.js +63 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +326 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.js +48 -0
- package/dist/endpoints.d.ts +42 -0
- package/dist/endpoints.js +142 -0
- package/dist/init.d.ts +47 -0
- package/dist/init.js +430 -0
- package/dist/keyfile.d.ts +20 -0
- package/dist/keyfile.js +60 -0
- package/dist/manifest.d.ts +49 -0
- package/dist/manifest.js +83 -0
- package/dist/picker.d.ts +55 -0
- package/dist/picker.js +330 -0
- package/dist/probe.d.ts +24 -0
- package/dist/probe.js +105 -0
- package/dist/prompt.d.ts +2 -0
- package/dist/prompt.js +45 -0
- package/dist/register.d.ts +55 -0
- package/dist/register.js +54 -0
- package/dist/signer.d.ts +32 -0
- package/dist/signer.js +40 -0
- package/dist/tui.d.ts +62 -0
- package/dist/tui.js +153 -0
- package/dist/verify.d.ts +32 -0
- package/dist/verify.js +71 -0
- package/dist/widgets.d.ts +91 -0
- package/dist/widgets.js +294 -0
- package/package.json +64 -4
package/dist/browser.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-wallet signer: the CLI serves one page on 127.0.0.1 and the page asks MetaMask (or any EIP-1193 wallet
|
|
3
|
+
* injected as window.ethereum) to approve each request. The key never leaves the wallet, and nothing outside this
|
|
4
|
+
* machine is involved: no relay, no project id.
|
|
5
|
+
*
|
|
6
|
+
* Protocol (all under /s/<token>, a random 192-bit path the CLI prints and opens):
|
|
7
|
+
* GET /s/<token> the page
|
|
8
|
+
* GET /s/<token>/next long poll: the next request { id, method, params, description }, or 204 after 25 s
|
|
9
|
+
* POST /s/<token>/result { id, result } or { id, error: { code?, message } }
|
|
10
|
+
*
|
|
11
|
+
* Hardening: bound to 127.0.0.1 only; the Host header must be this origin (DNS rebinding); POSTs must carry this
|
|
12
|
+
* Origin and a JSON body; the token is compared in constant time; every response is no-store and the page runs
|
|
13
|
+
* under a nonce CSP with connect-src 'self'. The CLI re-checks what comes back: a signature must recover to the
|
|
14
|
+
* connected address, and the page re-checks the chain and account before each transaction.
|
|
15
|
+
*/
|
|
16
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
17
|
+
import { spawn } from "node:child_process";
|
|
18
|
+
import { createServer } from "node:http";
|
|
19
|
+
import { getAddress, verifyMessage } from "viem";
|
|
20
|
+
export class WalletRejectedError extends Error {
|
|
21
|
+
code;
|
|
22
|
+
constructor(message, code) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.name = "WalletRejectedError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function openInBrowser(url) {
|
|
29
|
+
const [cmd, args] = process.platform === "win32" ? ["cmd", ["/c", "start", "", url]]
|
|
30
|
+
: process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
|
|
31
|
+
try {
|
|
32
|
+
spawn(cmd, args, { detached: true, stdio: "ignore" }).on("error", () => { }).unref();
|
|
33
|
+
}
|
|
34
|
+
catch { /* the URL is printed too */ }
|
|
35
|
+
}
|
|
36
|
+
const utf8Hex = (s) => ("0x" + Buffer.from(s, "utf8").toString("hex"));
|
|
37
|
+
export async function browserSigner(opts) {
|
|
38
|
+
const { network } = opts;
|
|
39
|
+
const token = randomBytes(24).toString("base64url");
|
|
40
|
+
const nonce = randomBytes(16).toString("base64");
|
|
41
|
+
const timeoutMs = opts.timeoutMs ?? 10 * 60_000;
|
|
42
|
+
const queue = [];
|
|
43
|
+
const waiters = [];
|
|
44
|
+
let nextId = 1;
|
|
45
|
+
let origin = "";
|
|
46
|
+
const server = createServer((req, res) => handle(req, res).catch(() => { if (!res.headersSent)
|
|
47
|
+
send(res, 500, "error"); }));
|
|
48
|
+
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(opts.port ?? 0, "127.0.0.1", () => resolve()); });
|
|
49
|
+
const port = server.address().port;
|
|
50
|
+
origin = `http://127.0.0.1:${port}`;
|
|
51
|
+
const allowedHosts = new Set([`127.0.0.1:${port}`, `localhost:${port}`]);
|
|
52
|
+
const allowedOrigins = new Set([`http://127.0.0.1:${port}`, `http://localhost:${port}`]);
|
|
53
|
+
const tokenBuf = Buffer.from(token);
|
|
54
|
+
function send(res, status, body, type = "text/plain; charset=utf-8") {
|
|
55
|
+
res.writeHead(status, {
|
|
56
|
+
"content-type": type,
|
|
57
|
+
"cache-control": "no-store",
|
|
58
|
+
"referrer-policy": "no-referrer",
|
|
59
|
+
"x-frame-options": "DENY",
|
|
60
|
+
"x-content-type-options": "nosniff",
|
|
61
|
+
"content-security-policy": `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'unsafe-inline'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`,
|
|
62
|
+
});
|
|
63
|
+
res.end(body);
|
|
64
|
+
}
|
|
65
|
+
const tokenOk = (t) => { const b = Buffer.from(t); return b.length === tokenBuf.length && timingSafeEqual(b, tokenBuf); };
|
|
66
|
+
function dispatch() {
|
|
67
|
+
while (waiters.length) {
|
|
68
|
+
const p = queue.find((q) => !q.dispatched);
|
|
69
|
+
if (!p)
|
|
70
|
+
return;
|
|
71
|
+
const res = waiters.shift();
|
|
72
|
+
if (res.writableEnded || res.destroyed)
|
|
73
|
+
continue;
|
|
74
|
+
p.dispatched = true;
|
|
75
|
+
send(res, 200, JSON.stringify({ id: p.id, method: p.method, params: p.params, description: p.description }), "application/json");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function readBody(req) {
|
|
79
|
+
let size = 0;
|
|
80
|
+
const chunks = [];
|
|
81
|
+
for await (const c of req) {
|
|
82
|
+
size += c.length;
|
|
83
|
+
if (size > 64 * 1024)
|
|
84
|
+
throw new Error("body too large");
|
|
85
|
+
chunks.push(c);
|
|
86
|
+
}
|
|
87
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
88
|
+
}
|
|
89
|
+
async function handle(req, res) {
|
|
90
|
+
if (!allowedHosts.has(req.headers.host ?? ""))
|
|
91
|
+
return send(res, 403, "forbidden host");
|
|
92
|
+
const m = /^\/s\/([A-Za-z0-9_-]+)(\/next|\/result)?$/.exec((req.url ?? "").split("?")[0]);
|
|
93
|
+
if (!m || !tokenOk(m[1]))
|
|
94
|
+
return send(res, 404, "not found");
|
|
95
|
+
const route = m[2] ?? "";
|
|
96
|
+
if (req.method === "GET" && route === "")
|
|
97
|
+
return send(res, 200, page(nonce, network), "text/html; charset=utf-8");
|
|
98
|
+
if (req.method === "GET" && route === "/next") {
|
|
99
|
+
waiters.push(res);
|
|
100
|
+
const t = setTimeout(() => { const i = waiters.indexOf(res); if (i >= 0) {
|
|
101
|
+
waiters.splice(i, 1);
|
|
102
|
+
res.writeHead(204, { "cache-control": "no-store" });
|
|
103
|
+
res.end();
|
|
104
|
+
} }, 25_000);
|
|
105
|
+
res.on("close", () => { clearTimeout(t); const i = waiters.indexOf(res); if (i >= 0)
|
|
106
|
+
waiters.splice(i, 1); });
|
|
107
|
+
return dispatch();
|
|
108
|
+
}
|
|
109
|
+
if (req.method === "POST" && route === "/result") {
|
|
110
|
+
if (!allowedOrigins.has(req.headers.origin ?? ""))
|
|
111
|
+
return send(res, 403, "forbidden origin");
|
|
112
|
+
if (!(req.headers["content-type"] ?? "").startsWith("application/json"))
|
|
113
|
+
return send(res, 415, "json only");
|
|
114
|
+
let msg;
|
|
115
|
+
try {
|
|
116
|
+
msg = JSON.parse(await readBody(req));
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return send(res, 400, "bad json");
|
|
120
|
+
}
|
|
121
|
+
const i = queue.findIndex((q) => q.id === msg.id && q.dispatched);
|
|
122
|
+
if (i < 0)
|
|
123
|
+
return send(res, 409, "no such request");
|
|
124
|
+
const [p] = queue.splice(i, 1);
|
|
125
|
+
clearTimeout(p.timer);
|
|
126
|
+
send(res, 204, "");
|
|
127
|
+
if (msg.error) {
|
|
128
|
+
const code = msg.error.code;
|
|
129
|
+
p.reject(code === 4001 ? new WalletRejectedError("you rejected the request in the wallet", code) : new Error(`wallet: ${msg.error.message ?? "request failed"}`));
|
|
130
|
+
}
|
|
131
|
+
else
|
|
132
|
+
p.resolve(msg.result);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
send(res, 405, "method not allowed");
|
|
136
|
+
}
|
|
137
|
+
function request(method, params, description) {
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const p = { id: nextId++, method, params, description, dispatched: false, resolve: resolve, reject };
|
|
140
|
+
if (method !== "close")
|
|
141
|
+
p.timer = setTimeout(() => {
|
|
142
|
+
const i = queue.indexOf(p);
|
|
143
|
+
if (i >= 0)
|
|
144
|
+
queue.splice(i, 1);
|
|
145
|
+
reject(new Error(`timed out after ${Math.round(timeoutMs / 60000)} min waiting for the wallet (is the page at ${origin}/s/… still open?)`));
|
|
146
|
+
}, timeoutMs);
|
|
147
|
+
queue.push(p);
|
|
148
|
+
dispatch();
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
const url = `${origin}/s/${token}`;
|
|
152
|
+
opts.onUrl?.(url);
|
|
153
|
+
(opts.open ?? openInBrowser)(url);
|
|
154
|
+
const shutdown = async () => {
|
|
155
|
+
for (const p of queue.splice(0)) {
|
|
156
|
+
clearTimeout(p.timer);
|
|
157
|
+
p.reject(new Error("signer closed"));
|
|
158
|
+
}
|
|
159
|
+
for (const w of waiters.splice(0)) {
|
|
160
|
+
try {
|
|
161
|
+
w.writeHead(204);
|
|
162
|
+
w.end();
|
|
163
|
+
}
|
|
164
|
+
catch { /* gone */ }
|
|
165
|
+
}
|
|
166
|
+
server.closeAllConnections?.();
|
|
167
|
+
await new Promise((r) => server.close(() => r()));
|
|
168
|
+
};
|
|
169
|
+
let address;
|
|
170
|
+
try {
|
|
171
|
+
const connected = await request("connect", chainParams(network), `Connect a wallet on ${network.chain.name}`);
|
|
172
|
+
address = getAddress(connected);
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
await shutdown();
|
|
176
|
+
throw e;
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
kind: "browser",
|
|
180
|
+
address,
|
|
181
|
+
async signMessage(message, description) {
|
|
182
|
+
const sig = await request("personal_sign", { message, hex: utf8Hex(message), address }, description);
|
|
183
|
+
if (!(await verifyMessage({ address, message, signature: sig })))
|
|
184
|
+
throw new Error(`the wallet returned a signature that does not recover to ${address}`);
|
|
185
|
+
return sig;
|
|
186
|
+
},
|
|
187
|
+
async sendTransaction(tx) {
|
|
188
|
+
const hash = await request("eth_sendTransaction", {
|
|
189
|
+
from: address, to: tx.to, data: tx.data, value: "0x" + (tx.value ?? 0n).toString(16), chainId: "0x" + network.chainId.toString(16),
|
|
190
|
+
}, tx.description);
|
|
191
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(hash ?? ""))
|
|
192
|
+
throw new Error("the wallet did not return a transaction hash");
|
|
193
|
+
return hash;
|
|
194
|
+
},
|
|
195
|
+
async close() {
|
|
196
|
+
const bye = request("close", null, "All done. You can close this tab.").catch(() => { });
|
|
197
|
+
await Promise.race([bye, new Promise((r) => setTimeout(r, 1500))]);
|
|
198
|
+
await shutdown();
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function chainParams(n) {
|
|
203
|
+
return {
|
|
204
|
+
chainId: "0x" + n.chainId.toString(16),
|
|
205
|
+
chainName: n.chain.name,
|
|
206
|
+
nativeCurrency: n.chain.nativeCurrency,
|
|
207
|
+
rpcUrls: n.rpcUrls,
|
|
208
|
+
blockExplorerUrls: [n.explorer],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/** The wallet page. Everything shown to the user goes through textContent; nothing from a request is parsed as HTML. */
|
|
212
|
+
function page(nonce, n) {
|
|
213
|
+
return `<!doctype html>
|
|
214
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
215
|
+
<title>agt · sign with your wallet</title>
|
|
216
|
+
<style>
|
|
217
|
+
:root { color-scheme: light dark; font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
218
|
+
body { max-width: 640px; margin: 48px auto; padding: 0 20px; }
|
|
219
|
+
h1 { font-size: 20px; margin: 0 0 4px; }
|
|
220
|
+
.muted { opacity: .7; }
|
|
221
|
+
.card { border: 1px solid #8884; border-radius: 10px; padding: 16px; margin: 20px 0; }
|
|
222
|
+
pre { white-space: pre-wrap; word-break: break-all; font-size: 12px; max-height: 320px; overflow: auto; background: #8881; padding: 10px; border-radius: 6px; }
|
|
223
|
+
button { font: inherit; padding: 8px 16px; border-radius: 8px; border: 1px solid #8886; cursor: pointer; }
|
|
224
|
+
.err { color: #d33; } .ok { color: #2a2; }
|
|
225
|
+
dt { font-weight: 600; margin-top: 6px; } dd { margin: 0; word-break: break-all; font-family: ui-monospace, monospace; font-size: 13px; }
|
|
226
|
+
</style></head>
|
|
227
|
+
<body>
|
|
228
|
+
<h1>agt command line</h1>
|
|
229
|
+
<div class="muted">Network: ${n.chain.name} (chain ${n.chainId}). Requests from the terminal appear here; approve each in your wallet.</div>
|
|
230
|
+
<div class="card"><div id="status">Waiting for the terminal…</div><div id="detail"></div><p id="action"></p></div>
|
|
231
|
+
<div class="muted" id="account"></div>
|
|
232
|
+
<script nonce="${nonce}">
|
|
233
|
+
(() => {
|
|
234
|
+
const base = location.pathname;
|
|
235
|
+
const $ = (id) => document.getElementById(id);
|
|
236
|
+
const eth = window.ethereum;
|
|
237
|
+
const setStatus = (text, cls) => { const s = $("status"); s.textContent = text; s.className = cls || ""; };
|
|
238
|
+
const clear = () => { $("detail").replaceChildren(); $("action").replaceChildren(); };
|
|
239
|
+
const pre = (text) => { const p = document.createElement("pre"); p.textContent = text; return p; };
|
|
240
|
+
const dl = (rows) => { const d = document.createElement("dl"); for (const [k, v] of rows) { const t = document.createElement("dt"); t.textContent = k; const x = document.createElement("dd"); x.textContent = v; d.append(t, x); } return d; };
|
|
241
|
+
let account = null, chain = null, stopped = false;
|
|
242
|
+
|
|
243
|
+
async function post(body) {
|
|
244
|
+
await fetch(base + "/result", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
245
|
+
}
|
|
246
|
+
function button(label) {
|
|
247
|
+
return new Promise((resolve) => { const b = document.createElement("button"); b.textContent = label; b.onclick = () => { b.disabled = true; resolve(); }; $("action").replaceChildren(b); });
|
|
248
|
+
}
|
|
249
|
+
async function ensureChain(params) {
|
|
250
|
+
const current = await eth.request({ method: "eth_chainId" });
|
|
251
|
+
if (current.toLowerCase() === params.chainId.toLowerCase()) return;
|
|
252
|
+
try { await eth.request({ method: "wallet_switchEthereumChain", params: [{ chainId: params.chainId }] }); }
|
|
253
|
+
catch (e) {
|
|
254
|
+
if (e && (e.code === 4902 || (e.data && e.data.originalError && e.data.originalError.code === 4902))) await eth.request({ method: "wallet_addEthereumChain", params: [params] });
|
|
255
|
+
else throw e;
|
|
256
|
+
}
|
|
257
|
+
const after = await eth.request({ method: "eth_chainId" });
|
|
258
|
+
if (after.toLowerCase() !== params.chainId.toLowerCase()) throw new Error("the wallet is on chain " + parseInt(after, 16) + ", expected " + parseInt(params.chainId, 16));
|
|
259
|
+
}
|
|
260
|
+
async function ensureAccount(expected) {
|
|
261
|
+
const accs = await eth.request({ method: "eth_accounts" });
|
|
262
|
+
if (!accs.length || accs[0].toLowerCase() !== expected.toLowerCase()) throw new Error("the wallet's selected account changed to " + (accs[0] || "none") + "; switch back to " + expected);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function run(r) {
|
|
266
|
+
clear();
|
|
267
|
+
if (r.method === "close") { stopped = true; setStatus(r.description, "ok"); await post({ id: r.id, result: true }); return; }
|
|
268
|
+
if (!eth) throw Object.assign(new Error("No browser wallet found. Install MetaMask, then reload this page."), { code: -1 });
|
|
269
|
+
setStatus(r.description);
|
|
270
|
+
if (r.method === "connect") {
|
|
271
|
+
chain = r.params;
|
|
272
|
+
$("detail").append(dl([["Network", chain.chainName + " (" + parseInt(chain.chainId, 16) + ")"]]));
|
|
273
|
+
await button("Connect wallet");
|
|
274
|
+
const accs = await eth.request({ method: "eth_requestAccounts" });
|
|
275
|
+
await ensureChain(chain);
|
|
276
|
+
account = accs[0];
|
|
277
|
+
$("account").textContent = "Connected: " + account;
|
|
278
|
+
return account;
|
|
279
|
+
}
|
|
280
|
+
if (r.method === "personal_sign") {
|
|
281
|
+
$("detail").append(dl([["Signing as", r.params.address]]), pre(r.params.message));
|
|
282
|
+
await ensureAccount(r.params.address);
|
|
283
|
+
return eth.request({ method: "personal_sign", params: [r.params.hex, r.params.address] });
|
|
284
|
+
}
|
|
285
|
+
if (r.method === "eth_sendTransaction") {
|
|
286
|
+
const p = r.params;
|
|
287
|
+
$("detail").append(dl([["From", p.from], ["To", p.to], ["Value (wei)", BigInt(p.value).toString()], ["Data", (p.data.length - 2) / 2 + " bytes"]]), pre(p.data));
|
|
288
|
+
await ensureChain(chain);
|
|
289
|
+
await ensureAccount(p.from);
|
|
290
|
+
const { chainId, ...tx } = p;
|
|
291
|
+
return eth.request({ method: "eth_sendTransaction", params: [tx] });
|
|
292
|
+
}
|
|
293
|
+
throw new Error("unknown request " + r.method);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function loop() {
|
|
297
|
+
while (!stopped) {
|
|
298
|
+
let r;
|
|
299
|
+
try { const res = await fetch(base + "/next", { cache: "no-store" }); if (res.status === 204) continue; if (!res.ok) throw new Error("terminal returned " + res.status); r = await res.json(); }
|
|
300
|
+
catch (e) { setStatus("Lost the terminal connection. If the command finished, close this tab.", "err"); return; }
|
|
301
|
+
try { const result = await run(r); if (r.method !== "close") { setStatus("Sent back to the terminal.", "ok"); await post({ id: r.id, result }); } }
|
|
302
|
+
catch (e) { setStatus((e && e.message) || String(e), "err"); await post({ id: r.id, error: { code: e && e.code, message: (e && e.message) || String(e) } }); }
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
loop();
|
|
306
|
+
})();
|
|
307
|
+
</script>
|
|
308
|
+
</body></html>`;
|
|
309
|
+
}
|
package/dist/chain.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Address, type Hex, type PublicClient } from "viem";
|
|
2
|
+
import { type Network } from "./config.js";
|
|
3
|
+
import type { Signer, TxRequest } from "./signer.js";
|
|
4
|
+
export declare function publicClient(n: Network): PublicClient;
|
|
5
|
+
export declare function normalizeLabel(input: string): string;
|
|
6
|
+
export declare const nodeOf: (label: string) => Hex;
|
|
7
|
+
export interface NameState {
|
|
8
|
+
label: string;
|
|
9
|
+
owner: Address | null;
|
|
10
|
+
resolver: Address | null;
|
|
11
|
+
}
|
|
12
|
+
export declare function nameState(pc: PublicClient, n: Network, label: string): Promise<NameState>;
|
|
13
|
+
/** The signer must own the name and the name must have a resolver, or every write would revert. */
|
|
14
|
+
export declare function assertOwner(s: NameState, signer: Address): Address;
|
|
15
|
+
export interface RecordsPlan {
|
|
16
|
+
manifest?: string;
|
|
17
|
+
endpoints?: {
|
|
18
|
+
protocol: string;
|
|
19
|
+
url: string;
|
|
20
|
+
}[];
|
|
21
|
+
addr?: Address;
|
|
22
|
+
wallet?: Address;
|
|
23
|
+
}
|
|
24
|
+
/** One multicall: pointer (skipped if unchanged), an endpoint per protocol, addr, agent wallet. */
|
|
25
|
+
export declare function recordCalls(label: string, plan: RecordsPlan, currentManifest?: string): Hex[];
|
|
26
|
+
export declare function describePlan(label: string, plan: RecordsPlan, currentManifest?: string): string[];
|
|
27
|
+
export declare const multicallTx: (resolver: Address, calls: Hex[], description: string) => TxRequest;
|
|
28
|
+
export declare function sendAndWait(pc: PublicClient, n: Network, signer: Signer, tx: TxRequest, log: (s: string) => void): Promise<Hex>;
|
|
29
|
+
/** What --dry-run prints for a transaction instead of sending it. */
|
|
30
|
+
export declare const dryTx: (tx: TxRequest, from: Address) => {
|
|
31
|
+
description: string;
|
|
32
|
+
from: `0x${string}`;
|
|
33
|
+
to: `0x${string}`;
|
|
34
|
+
value: string;
|
|
35
|
+
data: `0x${string}`;
|
|
36
|
+
};
|
package/dist/chain.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Chain reads, record-write batching, and send → receipt → one printed verification line. */
|
|
2
|
+
import { namehash } from "@agtnames/resolver";
|
|
3
|
+
import { createPublicClient, encodeFunctionData, fallback, http, zeroAddress } from "viem";
|
|
4
|
+
import { registryAbi, resolverAbi } from "./abi.js";
|
|
5
|
+
import { UsageError } from "./args.js";
|
|
6
|
+
import { txUrl } from "./config.js";
|
|
7
|
+
export function publicClient(n) {
|
|
8
|
+
return createPublicClient({ chain: n.chain, transport: n.rpcUrls.length > 1 ? fallback(n.rpcUrls.map((u) => http(u))) : http(n.rpcUrls[0]) });
|
|
9
|
+
}
|
|
10
|
+
export function normalizeLabel(input) {
|
|
11
|
+
const label = input.trim().toLowerCase().replace(/\.agt$/, "");
|
|
12
|
+
if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label))
|
|
13
|
+
throw new UsageError(`"${input}" is not a valid .agt name: 1–63 characters, a-z 0-9 and hyphens, no leading or trailing hyphen`);
|
|
14
|
+
return label;
|
|
15
|
+
}
|
|
16
|
+
export const nodeOf = (label) => namehash(`${label}.agt`);
|
|
17
|
+
export async function nameState(pc, n, label) {
|
|
18
|
+
const tokenId = await pc.readContract({ address: n.registry, abi: registryAbi, functionName: "tokenIdOf", args: [label] });
|
|
19
|
+
const owner = await pc.readContract({ address: n.registry, abi: registryAbi, functionName: "ownerOf", args: [tokenId] }).catch(() => null);
|
|
20
|
+
const resolver = owner ? await pc.readContract({ address: n.registry, abi: registryAbi, functionName: "resolverOf", args: [tokenId] }).catch(() => null) : null;
|
|
21
|
+
return { label, owner: owner && owner !== zeroAddress ? owner : null, resolver: resolver && resolver !== zeroAddress ? resolver : null };
|
|
22
|
+
}
|
|
23
|
+
/** The signer must own the name and the name must have a resolver, or every write would revert. */
|
|
24
|
+
export function assertOwner(s, signer) {
|
|
25
|
+
if (!s.owner)
|
|
26
|
+
throw new Error(`${s.label}.agt is not registered (run \`agt register ${s.label}\` first)`);
|
|
27
|
+
if (s.owner.toLowerCase() !== signer.toLowerCase())
|
|
28
|
+
throw new Error(`${s.label}.agt is owned by ${s.owner}, but you are signing as ${signer}. Sign with the owner's wallet.`);
|
|
29
|
+
if (!s.resolver)
|
|
30
|
+
throw new Error(`${s.label}.agt has no resolver (the name may have expired)`);
|
|
31
|
+
return s.resolver;
|
|
32
|
+
}
|
|
33
|
+
/** One multicall: pointer (skipped if unchanged), an endpoint per protocol, addr, agent wallet. */
|
|
34
|
+
export function recordCalls(label, plan, currentManifest = "") {
|
|
35
|
+
const node = nodeOf(label);
|
|
36
|
+
return [
|
|
37
|
+
...(plan.manifest && plan.manifest !== currentManifest ? [encodeFunctionData({ abi: resolverAbi, functionName: "setAgentManifest", args: [node, plan.manifest] })] : []),
|
|
38
|
+
...(plan.endpoints ?? []).map((e) => encodeFunctionData({ abi: resolverAbi, functionName: "setAgentEndpoint", args: [node, e.protocol, e.url] })),
|
|
39
|
+
...(plan.addr ? [encodeFunctionData({ abi: resolverAbi, functionName: "setAddr", args: [node, plan.addr] })] : []),
|
|
40
|
+
...(plan.wallet ? [encodeFunctionData({ abi: resolverAbi, functionName: "setAgentWallet", args: [node, plan.wallet] })] : []),
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
export function describePlan(label, plan, currentManifest = "") {
|
|
44
|
+
return [
|
|
45
|
+
...(plan.manifest ? [plan.manifest === currentManifest ? `manifest pointer already ${plan.manifest} (unchanged)` : `manifest → ${plan.manifest}`] : []),
|
|
46
|
+
...(plan.endpoints ?? []).map((e) => `endpoint ${e.protocol} → ${e.url}`),
|
|
47
|
+
...(plan.addr ? [`addr → ${plan.addr}`] : []),
|
|
48
|
+
...(plan.wallet ? [`agent wallet → ${plan.wallet}`] : []),
|
|
49
|
+
].map((l) => ` ${label}.agt ${l}`);
|
|
50
|
+
}
|
|
51
|
+
export const multicallTx = (resolver, calls, description) => ({ to: resolver, data: encodeFunctionData({ abi: resolverAbi, functionName: "multicall", args: [calls] }), description });
|
|
52
|
+
export async function sendAndWait(pc, n, signer, tx, log) {
|
|
53
|
+
log(`→ ${tx.description}${signer.kind === "browser" ? " (approve in your wallet)" : ""}`);
|
|
54
|
+
const hash = await signer.sendTransaction(tx);
|
|
55
|
+
log(` sent ${txUrl(n, hash)}`);
|
|
56
|
+
const rc = await pc.waitForTransactionReceipt({ hash, timeout: 180_000 });
|
|
57
|
+
if (rc.status !== "success")
|
|
58
|
+
throw new Error(`transaction reverted: ${txUrl(n, hash)}`);
|
|
59
|
+
log(` confirmed in block ${rc.blockNumber}`);
|
|
60
|
+
return hash;
|
|
61
|
+
}
|
|
62
|
+
/** What --dry-run prints for a transaction instead of sending it. */
|
|
63
|
+
export const dryTx = (tx, from) => ({ description: tx.description, from, to: tx.to, value: (tx.value ?? 0n).toString(), data: tx.data });
|
package/dist/cli.d.ts
ADDED