@aixyz/cli 0.7.0 → 0.9.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,753 @@
1
+ import { execFile } from "child_process";
2
+
3
+ export interface BrowserSignParams {
4
+ registryAddress: `0x${string}`;
5
+ calldata: `0x${string}`;
6
+ chainId: number;
7
+ chainName: string;
8
+ uri?: string;
9
+ gas?: bigint;
10
+ mode?: "register" | "update";
11
+ }
12
+
13
+ const TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
14
+
15
+ export async function signWithBrowser(params: BrowserSignParams): Promise<{ txHash: string }> {
16
+ const { registryAddress, calldata, chainId, chainName, uri, gas, mode } = params;
17
+
18
+ const nonce = crypto.randomUUID();
19
+ const html = buildHtml({ registryAddress, calldata, chainId, chainName, uri, gas, nonce, mode });
20
+
21
+ const { promise: resultPromise, resolve, reject } = Promise.withResolvers<{ txHash: string }>();
22
+ let settled = false;
23
+
24
+ function resolveOnce(result: { txHash: string }): void {
25
+ if (!settled) {
26
+ settled = true;
27
+ resolve(result);
28
+ }
29
+ }
30
+
31
+ function rejectOnce(error: Error): void {
32
+ if (!settled) {
33
+ settled = true;
34
+ reject(error);
35
+ }
36
+ }
37
+
38
+ const jsonHeaders = { "Content-Type": "application/json" };
39
+
40
+ const server = Bun.serve({
41
+ hostname: "localhost",
42
+ port: 0,
43
+ error(error) {
44
+ console.error(`Local server error: ${error.message}`);
45
+ rejectOnce(new Error(`Internal server error: ${error.message}`));
46
+ return new Response("Internal Server Error", { status: 500 });
47
+ },
48
+ async fetch(req) {
49
+ const url = new URL(req.url);
50
+
51
+ if (req.method === "GET" && url.pathname === "/") {
52
+ return new Response(html, { headers: { "Content-Type": "text/html" } });
53
+ }
54
+
55
+ if (req.method === "POST" && url.pathname === `/result/${nonce}`) {
56
+ if (settled) {
57
+ return new Response(JSON.stringify({ ok: true, ignored: true }), { headers: jsonHeaders });
58
+ }
59
+
60
+ let body: { txHash?: string; error?: string };
61
+ try {
62
+ body = (await req.json()) as { txHash?: string; error?: string };
63
+ } catch (parseError) {
64
+ const msg = `Received malformed response from browser wallet: ${parseError instanceof Error ? parseError.message : String(parseError)}`;
65
+ console.error(msg);
66
+ rejectOnce(new Error(msg));
67
+ return new Response(JSON.stringify({ error: msg }), { status: 400, headers: jsonHeaders });
68
+ }
69
+
70
+ if (body.txHash) {
71
+ resolveOnce({ txHash: body.txHash });
72
+ } else {
73
+ rejectOnce(new Error(body.error || "Unknown error from browser wallet"));
74
+ }
75
+ return new Response(JSON.stringify({ ok: true }), { headers: jsonHeaders });
76
+ }
77
+
78
+ return new Response("Not Found", { status: 404 });
79
+ },
80
+ });
81
+
82
+ const localUrl = `http://localhost:${server.port}`;
83
+ console.log(`\nOpening browser wallet at ${localUrl}`);
84
+ console.log(`This page will remain available for ${TIMEOUT_MS / 60_000} minutes.`);
85
+ console.log("If the browser doesn't open, visit the URL manually.\n");
86
+
87
+ openBrowser(localUrl);
88
+
89
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
90
+ const timeoutPromise = new Promise<never>((_, rej) => {
91
+ timeoutId = setTimeout(() => rej(new Error("Browser wallet timed out after 5 minutes")), TIMEOUT_MS);
92
+ });
93
+
94
+ try {
95
+ return await Promise.race([resultPromise, timeoutPromise]);
96
+ } finally {
97
+ clearTimeout(timeoutId);
98
+ server.stop();
99
+ }
100
+ }
101
+
102
+ function openBrowser(url: string): void {
103
+ const commands: Record<string, [string, ...string[]]> = {
104
+ darwin: ["open", url],
105
+ win32: ["cmd", "/c", "start", "", url],
106
+ };
107
+ const [cmd, ...args] = commands[process.platform] ?? ["xdg-open", url];
108
+
109
+ execFile(cmd, args, (err) => {
110
+ if (err) {
111
+ console.error(`\nCould not open browser automatically. Please open this URL manually: ${url}\n`);
112
+ }
113
+ });
114
+ }
115
+
116
+ export function escapeHtml(s: string): string {
117
+ return s
118
+ .replace(/&/g, "&amp;")
119
+ .replace(/</g, "&lt;")
120
+ .replace(/>/g, "&gt;")
121
+ .replace(/"/g, "&quot;")
122
+ .replace(/'/g, "&#039;");
123
+ }
124
+
125
+ /** JSON.stringify does not escape `</script>`, which breaks out of a script tag. */
126
+ export function safeJsonEmbed(value: unknown): string {
127
+ return JSON.stringify(value).replace(/</g, "\\u003c");
128
+ }
129
+
130
+ export function buildHtml(params: {
131
+ registryAddress: string;
132
+ calldata: string;
133
+ chainId: number;
134
+ chainName: string;
135
+ uri?: string;
136
+ gas?: bigint;
137
+ nonce: string;
138
+ mode?: "register" | "update";
139
+ }): string {
140
+ const { registryAddress, calldata, chainId, chainName, uri, gas, nonce, mode } = params;
141
+ const isUpdate = mode === "update";
142
+ const actionLabel = isUpdate ? "Update Agent" : "Register Agent";
143
+ const chainIdHex = `0x${chainId.toString(16)}`;
144
+
145
+ const displayUri = uri && uri.length > 80 ? uri.slice(0, 80) + "..." : (uri ?? "");
146
+
147
+ return `<!DOCTYPE html>
148
+ <html lang="en">
149
+ <head>
150
+ <meta charset="UTF-8">
151
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
152
+ <title>aixyz.sh – ERC-8004 ${actionLabel}</title>
153
+ <link rel="preconnect" href="https://fonts.googleapis.com">
154
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
155
+ <link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet">
156
+ <style>
157
+ :root {
158
+ --bg: #08080c;
159
+ --surface: #111118;
160
+ --surface-raised: #18181f;
161
+ --border: #222230;
162
+ --border-hover: #3a3a50;
163
+ --text: #c8c8d0;
164
+ --text-dim: #6a6a78;
165
+ --text-bright: #eeeef2;
166
+ --accent: #6e56cf;
167
+ --accent-dim: rgba(110,86,207,0.12);
168
+ --green: #3dd68c;
169
+ --green-dim: rgba(61,214,140,0.1);
170
+ --red: #e5484d;
171
+ --red-dim: rgba(229,72,77,0.1);
172
+ --blue: #52a9ff;
173
+ --blue-dim: rgba(82,169,255,0.1);
174
+ --mono: 'DM Mono', 'SF Mono', 'Fira Code', monospace;
175
+ --sans: 'DM Sans', system-ui, sans-serif;
176
+ --radius: 8px;
177
+ }
178
+
179
+ * { margin: 0; padding: 0; box-sizing: border-box; }
180
+
181
+ body {
182
+ font-family: var(--sans);
183
+ background: var(--bg);
184
+ color: var(--text);
185
+ min-height: 100vh;
186
+ display: flex;
187
+ align-items: center;
188
+ justify-content: center;
189
+ padding: 2rem;
190
+ }
191
+
192
+ .container {
193
+ max-width: 420px;
194
+ width: 100%;
195
+ }
196
+
197
+ .header {
198
+ margin-bottom: 1.75rem;
199
+ animation: fadeIn 0.4s ease-out;
200
+ }
201
+
202
+ .brand {
203
+ font-family: var(--mono);
204
+ font-size: 0.7rem;
205
+ font-weight: 500;
206
+ letter-spacing: 0.08em;
207
+ text-transform: uppercase;
208
+ color: var(--accent);
209
+ margin-bottom: 0.75rem;
210
+ }
211
+
212
+ h1 {
213
+ font-family: var(--sans);
214
+ font-size: 1.35rem;
215
+ font-weight: 600;
216
+ color: var(--text-bright);
217
+ letter-spacing: -0.01em;
218
+ }
219
+
220
+ .details {
221
+ background: var(--surface);
222
+ border: 1px solid var(--border);
223
+ border-radius: var(--radius);
224
+ padding: 0;
225
+ margin-bottom: 1.5rem;
226
+ overflow: hidden;
227
+ animation: fadeIn 0.4s ease-out 0.05s both;
228
+ }
229
+
230
+ .detail-row {
231
+ display: flex;
232
+ justify-content: space-between;
233
+ align-items: baseline;
234
+ padding: 0.65rem 0.85rem;
235
+ gap: 1rem;
236
+ }
237
+
238
+ .detail-row + .detail-row {
239
+ border-top: 1px solid var(--border);
240
+ }
241
+
242
+ .detail-label {
243
+ font-family: var(--mono);
244
+ font-size: 0.7rem;
245
+ font-weight: 400;
246
+ color: var(--text-dim);
247
+ text-transform: uppercase;
248
+ letter-spacing: 0.04em;
249
+ white-space: nowrap;
250
+ flex-shrink: 0;
251
+ }
252
+
253
+ .detail-value {
254
+ font-family: var(--mono);
255
+ font-size: 0.75rem;
256
+ font-weight: 400;
257
+ color: var(--text);
258
+ text-align: right;
259
+ word-break: break-all;
260
+ line-height: 1.5;
261
+ }
262
+
263
+ /* --- Wallet section --- */
264
+ #walletSection {
265
+ animation: fadeIn 0.4s ease-out 0.1s both;
266
+ }
267
+
268
+ .section-label {
269
+ font-family: var(--mono);
270
+ font-size: 0.7rem;
271
+ font-weight: 400;
272
+ color: var(--text-dim);
273
+ text-transform: uppercase;
274
+ letter-spacing: 0.04em;
275
+ margin-bottom: 0.6rem;
276
+ }
277
+
278
+ #walletList { margin-bottom: 0; }
279
+
280
+ .wallet-btn {
281
+ display: flex;
282
+ align-items: center;
283
+ gap: 0.7rem;
284
+ width: 100%;
285
+ padding: 0.7rem 0.85rem;
286
+ background: var(--surface);
287
+ border: 1px solid var(--border);
288
+ color: var(--text);
289
+ font-family: var(--sans);
290
+ font-size: 0.85rem;
291
+ font-weight: 500;
292
+ cursor: pointer;
293
+ transition: border-color 0.15s, background 0.15s;
294
+ }
295
+
296
+ .wallet-btn:first-child { border-radius: var(--radius) var(--radius) 0 0; }
297
+ .wallet-btn:last-child { border-radius: 0 0 var(--radius) var(--radius); }
298
+ .wallet-btn:only-child { border-radius: var(--radius); }
299
+ .wallet-btn + .wallet-btn { border-top: none; }
300
+
301
+ .wallet-btn:hover:not(:disabled) {
302
+ background: var(--surface-raised);
303
+ border-color: var(--border-hover);
304
+ }
305
+ .wallet-btn:hover:not(:disabled) + .wallet-btn {
306
+ border-top-color: transparent;
307
+ }
308
+ .wallet-btn:disabled { opacity: 0.35; cursor: not-allowed; }
309
+
310
+ .wallet-btn img {
311
+ width: 24px;
312
+ height: 24px;
313
+ border-radius: 5px;
314
+ flex-shrink: 0;
315
+ }
316
+
317
+ .wallet-btn .arrow {
318
+ margin-left: auto;
319
+ color: var(--text-dim);
320
+ font-size: 0.8rem;
321
+ transition: transform 0.15s;
322
+ }
323
+ .wallet-btn:hover:not(:disabled) .arrow { transform: translateX(2px); }
324
+
325
+ .legacy-btn {
326
+ width: 100%;
327
+ padding: 0.7rem 0.85rem;
328
+ background: var(--surface);
329
+ border: 1px solid var(--border);
330
+ border-radius: var(--radius);
331
+ color: var(--text);
332
+ font-family: var(--sans);
333
+ font-size: 0.85rem;
334
+ font-weight: 500;
335
+ cursor: pointer;
336
+ transition: border-color 0.15s, background 0.15s;
337
+ }
338
+ .legacy-btn:hover {
339
+ background: var(--surface-raised);
340
+ border-color: var(--border-hover);
341
+ }
342
+
343
+ /* --- Connected state --- */
344
+ #walletInfo {
345
+ display: none;
346
+ background: var(--surface);
347
+ border: 1px solid var(--border);
348
+ border-radius: var(--radius);
349
+ padding: 0.7rem 0.85rem;
350
+ margin-bottom: 1rem;
351
+ animation: fadeIn 0.3s ease-out;
352
+ }
353
+
354
+ .connected-row {
355
+ display: flex;
356
+ align-items: center;
357
+ justify-content: space-between;
358
+ gap: 0.75rem;
359
+ }
360
+
361
+ .connected-addr {
362
+ font-family: var(--mono);
363
+ font-size: 0.75rem;
364
+ color: var(--text);
365
+ word-break: break-all;
366
+ line-height: 1.5;
367
+ }
368
+
369
+ .connected-dot {
370
+ width: 6px;
371
+ height: 6px;
372
+ border-radius: 50%;
373
+ background: var(--green);
374
+ flex-shrink: 0;
375
+ animation: pulse 2s ease-in-out infinite;
376
+ }
377
+
378
+ #disconnectBtn {
379
+ width: auto;
380
+ padding: 0.3rem 0.6rem;
381
+ background: transparent;
382
+ border: 1px solid var(--border);
383
+ border-radius: 5px;
384
+ color: var(--text-dim);
385
+ font-family: var(--mono);
386
+ font-size: 0.65rem;
387
+ font-weight: 400;
388
+ letter-spacing: 0.02em;
389
+ cursor: pointer;
390
+ transition: color 0.15s, border-color 0.15s;
391
+ flex-shrink: 0;
392
+ }
393
+ #disconnectBtn:hover {
394
+ color: var(--red);
395
+ border-color: var(--red);
396
+ }
397
+
398
+ /* --- Register button --- */
399
+ #registerBtn {
400
+ display: none;
401
+ width: 100%;
402
+ padding: 0.75rem;
403
+ background: var(--accent);
404
+ border: none;
405
+ border-radius: var(--radius);
406
+ color: #fff;
407
+ font-family: var(--sans);
408
+ font-size: 0.85rem;
409
+ font-weight: 600;
410
+ cursor: pointer;
411
+ transition: opacity 0.15s;
412
+ }
413
+ #registerBtn:hover:not(:disabled) { opacity: 0.88; }
414
+ #registerBtn:disabled { opacity: 0.35; cursor: not-allowed; }
415
+
416
+ /* --- Status --- */
417
+ .status {
418
+ margin-top: 1rem;
419
+ padding: 0.65rem 0.85rem;
420
+ border-radius: var(--radius);
421
+ font-family: var(--mono);
422
+ font-size: 0.72rem;
423
+ font-weight: 400;
424
+ line-height: 1.5;
425
+ display: none;
426
+ word-break: break-all;
427
+ animation: fadeIn 0.2s ease-out;
428
+ }
429
+ .status.error {
430
+ background: var(--red-dim);
431
+ border: 1px solid rgba(229,72,77,0.15);
432
+ color: var(--red);
433
+ display: block;
434
+ }
435
+ .status.success {
436
+ background: var(--green-dim);
437
+ border: 1px solid rgba(61,214,140,0.12);
438
+ color: var(--green);
439
+ display: block;
440
+ }
441
+ .status.info {
442
+ background: var(--blue-dim);
443
+ border: 1px solid rgba(82,169,255,0.12);
444
+ color: var(--blue);
445
+ display: block;
446
+ }
447
+
448
+ #discovering {
449
+ font-family: var(--mono);
450
+ color: var(--text-dim);
451
+ font-size: 0.72rem;
452
+ margin-bottom: 0.75rem;
453
+ letter-spacing: 0.01em;
454
+ }
455
+
456
+ #discovering::after {
457
+ content: '';
458
+ animation: dots 1.5s steps(4, end) infinite;
459
+ }
460
+
461
+ @keyframes dots {
462
+ 0% { content: ''; }
463
+ 25% { content: '.'; }
464
+ 50% { content: '..'; }
465
+ 75% { content: '...'; }
466
+ }
467
+
468
+ @keyframes fadeIn {
469
+ from { opacity: 0; transform: translateY(4px); }
470
+ to { opacity: 1; transform: translateY(0); }
471
+ }
472
+
473
+ @keyframes pulse {
474
+ 0%, 100% { opacity: 1; }
475
+ 50% { opacity: 0.4; }
476
+ }
477
+ </style>
478
+ </head>
479
+ <body>
480
+ <div class="container">
481
+ <div class="header">
482
+ <div class="brand">aixyz erc-8004</div>
483
+ <h1>${actionLabel}</h1>
484
+ </div>
485
+
486
+ <div class="details" id="details">
487
+ <div class="detail-row">
488
+ <span class="detail-label">Chain</span>
489
+ <span class="detail-value">${escapeHtml(chainName)} (${chainId})</span>
490
+ </div>
491
+ <div class="detail-row">
492
+ <span class="detail-label">Registry</span>
493
+ <span class="detail-value">${escapeHtml(registryAddress)}</span>
494
+ </div>
495
+ ${
496
+ uri
497
+ ? `<div class="detail-row">
498
+ <span class="detail-label">URI</span>
499
+ <span class="detail-value">${escapeHtml(displayUri)}</span>
500
+ </div>`
501
+ : ""
502
+ }
503
+ </div>
504
+
505
+ <div id="walletInfo">
506
+ <div class="connected-row">
507
+ <div style="display:flex;align-items:center;gap:0.5rem;min-width:0;">
508
+ <div class="connected-dot"></div>
509
+ <span class="connected-addr" id="addrDisplay"></span>
510
+ </div>
511
+ <button id="disconnectBtn" type="button">disconnect</button>
512
+ </div>
513
+ </div>
514
+
515
+ <div id="walletSection">
516
+ <div class="section-label" id="discovering">Discovering wallets</div>
517
+ <div id="walletList"></div>
518
+ </div>
519
+
520
+ <button id="registerBtn" disabled>${actionLabel}</button>
521
+
522
+ <div class="status" id="status"></div>
523
+ </div>
524
+
525
+ <script>
526
+ const REGISTRY = ${safeJsonEmbed(registryAddress)};
527
+ const CALLDATA = ${safeJsonEmbed(calldata)};
528
+ const CHAIN_ID_HEX = ${safeJsonEmbed(chainIdHex)};
529
+ const CHAIN_ID = ${chainId};
530
+ const GAS = ${gas ? safeJsonEmbed(`0x${gas.toString(16)}`) : "undefined"};
531
+ const ACTION_LABEL = ${safeJsonEmbed(actionLabel)};
532
+
533
+ const registerBtn = document.getElementById("registerBtn");
534
+ const statusEl = document.getElementById("status");
535
+ const walletInfo = document.getElementById("walletInfo");
536
+ const addrDisplay = document.getElementById("addrDisplay");
537
+ const walletListEl = document.getElementById("walletList");
538
+ const walletSectionEl = document.getElementById("walletSection");
539
+ const discoveringEl = document.getElementById("discovering");
540
+ const disconnectBtn = document.getElementById("disconnectBtn");
541
+
542
+ let account = null;
543
+ let selectedProvider = null;
544
+
545
+ disconnectBtn.addEventListener("click", () => {
546
+ account = null;
547
+ selectedProvider = null;
548
+ walletInfo.style.display = "none";
549
+ registerBtn.style.display = "none";
550
+ registerBtn.disabled = true;
551
+ registerBtn.textContent = ACTION_LABEL;
552
+ walletSectionEl.style.display = "";
553
+ statusEl.className = "status";
554
+ if (discoveredWallets.size > 0) {
555
+ renderWalletList();
556
+ } else if (window.ethereum) {
557
+ showLegacyConnect();
558
+ }
559
+ });
560
+
561
+ function setStatus(msg, type) {
562
+ statusEl.textContent = msg;
563
+ statusEl.className = "status " + type;
564
+ }
565
+
566
+ // --- EIP-6963 wallet discovery ---
567
+ const discoveredWallets = new Map(); // keyed by rdns for dedup
568
+
569
+ window.addEventListener("eip6963:announceProvider", (event) => {
570
+ const { info, provider } = event.detail;
571
+ if (!info || !info.rdns) return;
572
+ if (discoveredWallets.has(info.rdns)) return;
573
+ discoveredWallets.set(info.rdns, { info, provider });
574
+ renderWalletList();
575
+ });
576
+
577
+ window.dispatchEvent(new Event("eip6963:requestProvider"));
578
+
579
+ // Fallback after 500ms if no EIP-6963 wallets discovered
580
+ setTimeout(() => {
581
+ if (discoveredWallets.size > 0) return;
582
+ discoveringEl.style.display = "none";
583
+
584
+ if (window.ethereum) {
585
+ showLegacyConnect();
586
+ } else {
587
+ setStatus("No wallet detected. Install a browser wallet extension.", "error");
588
+ }
589
+ }, 500);
590
+
591
+ function renderWalletList() {
592
+ discoveringEl.style.display = "none";
593
+ walletListEl.replaceChildren();
594
+
595
+ for (const [rdns, detail] of discoveredWallets) {
596
+ const btn = document.createElement("button");
597
+ btn.className = "wallet-btn";
598
+ btn.type = "button";
599
+
600
+ if (detail.info.icon && /^data:image\\//.test(detail.info.icon)) {
601
+ const img = document.createElement("img");
602
+ img.src = detail.info.icon;
603
+ img.alt = "";
604
+ img.width = 24;
605
+ img.height = 24;
606
+ btn.appendChild(img);
607
+ }
608
+
609
+ const label = document.createElement("span");
610
+ label.textContent = detail.info.name || rdns;
611
+ btn.appendChild(label);
612
+
613
+ const arrow = document.createElement("span");
614
+ arrow.className = "arrow";
615
+ arrow.textContent = "\\u2192";
616
+ btn.appendChild(arrow);
617
+
618
+ btn.addEventListener("click", () => connectWallet(detail, btn));
619
+ walletListEl.appendChild(btn);
620
+ }
621
+ }
622
+
623
+ function showLegacyConnect() {
624
+ discoveringEl.style.display = "none";
625
+ walletListEl.replaceChildren();
626
+ const btn = document.createElement("button");
627
+ btn.className = "legacy-btn";
628
+ btn.type = "button";
629
+ btn.textContent = "Connect Wallet";
630
+ btn.addEventListener("click", () => {
631
+ connectWallet({ info: { name: "Browser Wallet", rdns: "_legacy" }, provider: window.ethereum }, btn);
632
+ });
633
+ walletListEl.appendChild(btn);
634
+ }
635
+
636
+ async function connectWallet(detail, btn) {
637
+ try {
638
+ // Disable all wallet buttons while connecting
639
+ walletListEl.querySelectorAll("button").forEach(b => { b.disabled = true; });
640
+ btn.textContent = "Connecting...";
641
+
642
+ selectedProvider = detail.provider;
643
+ const accounts = await selectedProvider.request({ method: "eth_requestAccounts" });
644
+ account = accounts[0];
645
+
646
+ // Check chain
647
+ const currentChainId = await selectedProvider.request({ method: "eth_chainId" });
648
+ if (currentChainId !== CHAIN_ID_HEX) {
649
+ setStatus("Switching chain...", "info");
650
+ try {
651
+ await selectedProvider.request({
652
+ method: "wallet_switchEthereumChain",
653
+ params: [{ chainId: CHAIN_ID_HEX }],
654
+ });
655
+ } catch (switchErr) {
656
+ if (switchErr.code === 4902) {
657
+ setStatus("Chain not found in wallet. Please add it manually and try again.", "error");
658
+ walletListEl.querySelectorAll("button").forEach(b => { b.disabled = false; });
659
+ renderWalletList();
660
+ return;
661
+ }
662
+ throw switchErr;
663
+ }
664
+ }
665
+
666
+ addrDisplay.textContent = account;
667
+ walletInfo.style.display = "block";
668
+ walletSectionEl.style.display = "none";
669
+ registerBtn.style.display = "block";
670
+ registerBtn.disabled = false;
671
+ setStatus("Wallet connected. Ready to ${isUpdate ? "update" : "register"}.", "success");
672
+
673
+ // Listen for account/chain changes on the selected provider
674
+ if (selectedProvider.on) {
675
+ selectedProvider.on("accountsChanged", () => location.reload());
676
+ selectedProvider.on("chainChanged", () => location.reload());
677
+ }
678
+ } catch (err) {
679
+ if (err.code === 4001) {
680
+ setStatus("Connection rejected by user.", "error");
681
+ } else {
682
+ setStatus("Connection failed: " + err.message, "error");
683
+ }
684
+ walletListEl.querySelectorAll("button").forEach(b => { b.disabled = false; });
685
+ renderWalletList();
686
+ }
687
+ }
688
+
689
+ registerBtn.addEventListener("click", async () => {
690
+ if (!selectedProvider || !account) return;
691
+ try {
692
+ registerBtn.disabled = true;
693
+ registerBtn.textContent = "Sign in wallet...";
694
+ setStatus("Please sign the transaction in your wallet.", "info");
695
+
696
+ const txParams = { from: account, to: REGISTRY, data: CALLDATA };
697
+ if (GAS) txParams.gas = GAS;
698
+
699
+ const txHash = await selectedProvider.request({
700
+ method: "eth_sendTransaction",
701
+ params: [txParams],
702
+ });
703
+
704
+ if (typeof txHash !== "string" || !/^0x[0-9a-f]{64}$/i.test(txHash)) {
705
+ throw new Error("Wallet returned invalid transaction hash");
706
+ }
707
+
708
+ const explorers = { 1: "https://etherscan.io", 11155111: "https://sepolia.etherscan.io", 84532: "https://sepolia.basescan.org" };
709
+ const explorerBase = explorers[CHAIN_ID];
710
+
711
+ statusEl.textContent = "";
712
+ const msgDiv = document.createElement("div");
713
+ msgDiv.style.marginBottom = "0.5rem";
714
+ msgDiv.appendChild(document.createTextNode("Transaction sent! "));
715
+ if (explorerBase) {
716
+ const link = document.createElement("a");
717
+ link.href = explorerBase + "/tx/" + txHash;
718
+ link.target = "_blank";
719
+ link.rel = "noopener";
720
+ link.style.color = "inherit";
721
+ link.style.textDecoration = "underline";
722
+ link.textContent = explorerBase + "/tx/" + txHash;
723
+ msgDiv.appendChild(link);
724
+ } else {
725
+ msgDiv.appendChild(document.createTextNode(txHash));
726
+ }
727
+ const hintDiv = document.createElement("div");
728
+ hintDiv.style.color = "var(--text-dim)";
729
+ hintDiv.textContent = "You can safely close this page and return to the CLI.";
730
+ statusEl.appendChild(msgDiv);
731
+ statusEl.appendChild(hintDiv);
732
+ statusEl.className = "status success";
733
+ registerBtn.textContent = "Sent!";
734
+
735
+ await fetch("/result/" + ${safeJsonEmbed(nonce)}, {
736
+ method: "POST",
737
+ headers: { "Content-Type": "application/json" },
738
+ body: JSON.stringify({ txHash }),
739
+ });
740
+ } catch (err) {
741
+ if (err.code === 4001) {
742
+ setStatus("Transaction rejected. You can try again.", "error");
743
+ } else {
744
+ setStatus("Failed: " + err.message + " — You can try again.", "error");
745
+ }
746
+ registerBtn.disabled = false;
747
+ registerBtn.textContent = ACTION_LABEL;
748
+ }
749
+ });
750
+ </script>
751
+ </body>
752
+ </html>`;
753
+ }