@aixyz/cli 0.8.0 → 0.10.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/bin.ts +19 -234
- package/build/AixyzConfigPlugin.ts +1 -1
- package/build/AixyzServerPlugin.ts +10 -0
- package/build/index.ts +36 -1
- package/dev/index.ts +7 -1
- package/package.json +3 -3
- package/register/index.ts +135 -0
- package/register/register.test.ts +0 -9
- package/register/register.ts +40 -14
- package/register/update.test.ts +47 -0
- package/register/{set-agent-uri.ts → update.ts} +39 -46
- package/register/utils/chain.ts +4 -5
- package/register/utils/erc8004-file.ts +65 -0
- package/register/utils/prompt.ts +52 -9
- package/register/wallet/browser.test.ts +18 -0
- package/register/wallet/browser.ts +15 -10
- package/register/wallet/index.ts +2 -3
- package/register/wallet/keystore.test.ts +1 -1
- package/register/wallet/keystore.ts +2 -3
- package/register/wallet/privatekey.ts +8 -3
- package/register/wallet/sign.ts +8 -5
- package/register/README.md +0 -101
- package/register/set-agent-uri.test.ts +0 -156
- package/register/utils.test.ts +0 -154
- package/register/utils.ts +0 -55
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { CHAIN_ID, getIdentityRegistryAddress } from "@aixyz/erc-8004";
|
|
3
|
+
import { deriveAgentUri } from "./utils/prompt";
|
|
4
|
+
|
|
5
|
+
describe("update command chain configuration", () => {
|
|
6
|
+
test("sepolia chain ID is correct", () => {
|
|
7
|
+
expect(CHAIN_ID.SEPOLIA).toStrictEqual(11155111);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("base-sepolia chain ID is correct", () => {
|
|
11
|
+
expect(CHAIN_ID.BASE_SEPOLIA).toStrictEqual(84532);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("identity registry address is returned for sepolia", () => {
|
|
15
|
+
const address = getIdentityRegistryAddress(CHAIN_ID.SEPOLIA);
|
|
16
|
+
expect(address).toMatch(/^0x[a-fA-F0-9]{40}$/);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("identity registry address is returned for base-sepolia", () => {
|
|
20
|
+
const address = getIdentityRegistryAddress(CHAIN_ID.BASE_SEPOLIA);
|
|
21
|
+
expect(address).toMatch(/^0x[a-fA-F0-9]{40}$/);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("throws for unsupported chain ID", () => {
|
|
25
|
+
expect(() => getIdentityRegistryAddress(999999)).toThrow("Unsupported chain ID");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("deriveAgentUri", () => {
|
|
30
|
+
test("appends /_aixyz/erc-8004.json to base URL", () => {
|
|
31
|
+
expect(deriveAgentUri("https://my-agent.example.com")).toBe("https://my-agent.example.com/_aixyz/erc-8004.json");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("strips trailing slash before appending", () => {
|
|
35
|
+
expect(deriveAgentUri("https://my-agent.example.com/")).toBe("https://my-agent.example.com/_aixyz/erc-8004.json");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("strips multiple trailing slashes", () => {
|
|
39
|
+
expect(deriveAgentUri("https://my-agent.example.com///")).toBe("https://my-agent.example.com/_aixyz/erc-8004.json");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("preserves path segments", () => {
|
|
43
|
+
expect(deriveAgentUri("https://example.com/agents/my-agent")).toBe(
|
|
44
|
+
"https://example.com/agents/my-agent/_aixyz/erc-8004.json",
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -2,66 +2,59 @@ import { encodeFunctionData, formatEther, parseEventLogs, type Chain, type Log }
|
|
|
2
2
|
import { IdentityRegistryAbi } from "@aixyz/erc-8004";
|
|
3
3
|
import { selectWalletMethod } from "./wallet";
|
|
4
4
|
import { signTransaction } from "./wallet/sign";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
resolveChainConfig,
|
|
8
|
-
selectChain,
|
|
9
|
-
resolveRegistryAddress,
|
|
10
|
-
validateBrowserRpcConflict,
|
|
11
|
-
getExplorerUrl,
|
|
12
|
-
} from "./utils/chain";
|
|
5
|
+
import { resolveChainConfig, validateBrowserRpcConflict, getExplorerUrl, CHAINS } from "./utils/chain";
|
|
13
6
|
import { writeResultJson } from "./utils/result";
|
|
14
7
|
import { label, truncateUri, broadcastAndConfirm, logSignResult } from "./utils/transaction";
|
|
15
|
-
import {
|
|
8
|
+
import { promptAgentUrl, promptSelectRegistration, deriveAgentUri } from "./utils/prompt";
|
|
9
|
+
import { readRegistrations } from "./utils/erc8004-file";
|
|
10
|
+
import { confirm } from "@inquirer/prompts";
|
|
16
11
|
import chalk from "chalk";
|
|
17
12
|
import boxen from "boxen";
|
|
18
13
|
import type { BaseOptions } from "./index";
|
|
19
|
-
import { promptAgentId, promptUri } from "./utils/prompt";
|
|
20
14
|
|
|
21
|
-
export interface
|
|
22
|
-
|
|
23
|
-
uri?: string;
|
|
15
|
+
export interface UpdateOptions extends BaseOptions {
|
|
16
|
+
url?: string;
|
|
24
17
|
}
|
|
25
18
|
|
|
26
|
-
async function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
throw new CliError("Aborted.");
|
|
19
|
+
export async function update(options: UpdateOptions): Promise<void> {
|
|
20
|
+
// Step 1: Read registrations from app/erc-8004.ts
|
|
21
|
+
const registrations = await readRegistrations();
|
|
22
|
+
|
|
23
|
+
if (registrations.length === 0) {
|
|
24
|
+
throw new Error("No registrations found in app/erc-8004.ts. Run `aixyz erc-8004 register` first.");
|
|
33
25
|
}
|
|
34
|
-
}
|
|
35
26
|
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
27
|
+
// Step 2: Select which registration to update
|
|
28
|
+
const selected = await promptSelectRegistration(registrations);
|
|
29
|
+
|
|
30
|
+
// Step 3: Derive chain info from agentRegistry (eip155:<chainId>:<address>)
|
|
31
|
+
const parts = selected.agentRegistry.split(":");
|
|
32
|
+
if (parts.length < 3 || parts[0] !== "eip155") {
|
|
33
|
+
throw new Error(`Invalid agentRegistry format: ${selected.agentRegistry}. Expected eip155:<chainId>:<address>`);
|
|
40
34
|
}
|
|
41
|
-
}
|
|
42
35
|
|
|
43
|
-
|
|
44
|
-
const
|
|
36
|
+
const chainId = Number(parts[1]);
|
|
37
|
+
const registryAddress = parts.slice(2).join(":") as `0x${string}`;
|
|
38
|
+
const chainName = Object.entries(CHAINS).find(([, config]) => config.chainId === chainId)?.[0] ?? `chain-${chainId}`;
|
|
45
39
|
const chainConfig = resolveChainConfig(chainName);
|
|
46
40
|
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
// Step 4: Get new agent URL and derive URI
|
|
42
|
+
const agentUrl = options.url ?? (await promptAgentUrl());
|
|
43
|
+
const resolvedUri = deriveAgentUri(agentUrl);
|
|
49
44
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
console.log(`Resolved ${uri} to data URI (${resolvedUri.length} chars)`);
|
|
45
|
+
const yes = await confirm({
|
|
46
|
+
message: `Will update URI to: ${chalk.cyan(resolvedUri)} — confirm?`,
|
|
47
|
+
default: true,
|
|
48
|
+
});
|
|
49
|
+
if (!yes) {
|
|
50
|
+
throw new Error("Aborted.");
|
|
57
51
|
}
|
|
58
52
|
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
// Step 5: Encode transaction
|
|
61
54
|
const data = encodeFunctionData({
|
|
62
55
|
abi: IdentityRegistryAbi,
|
|
63
56
|
functionName: "setAgentURI",
|
|
64
|
-
args: [BigInt(agentId), resolvedUri],
|
|
57
|
+
args: [BigInt(selected.agentId), resolvedUri],
|
|
65
58
|
});
|
|
66
59
|
|
|
67
60
|
const printTxDetails = (header: string) => {
|
|
@@ -71,7 +64,7 @@ export async function setAgentUri(options: SetAgentUriOptions): Promise<void> {
|
|
|
71
64
|
console.log(` ${label("Data")}${data.slice(0, 10)}${chalk.dim("\u2026" + (data.length - 2) / 2 + " bytes")}`);
|
|
72
65
|
console.log(` ${label("Chain")}${chainName}`);
|
|
73
66
|
console.log(` ${label("Function")}setAgentURI(uint256 agentId, string calldata newURI)`);
|
|
74
|
-
console.log(` ${label("Agent ID")}${agentId}`);
|
|
67
|
+
console.log(` ${label("Agent ID")}${selected.agentId}`);
|
|
75
68
|
console.log(` ${label("URI")}${truncateUri(resolvedUri)}`);
|
|
76
69
|
console.log("");
|
|
77
70
|
};
|
|
@@ -98,7 +91,7 @@ export async function setAgentUri(options: SetAgentUriOptions): Promise<void> {
|
|
|
98
91
|
chain: chainConfig.chain,
|
|
99
92
|
rpcUrl: options.rpcUrl,
|
|
100
93
|
options: {
|
|
101
|
-
browser: { chainId: chainConfig.chainId, chainName, uri: resolvedUri },
|
|
94
|
+
browser: { chainId: chainConfig.chainId, chainName, uri: resolvedUri, mode: "update" },
|
|
102
95
|
},
|
|
103
96
|
});
|
|
104
97
|
logSignResult(walletMethod.type, result);
|
|
@@ -112,11 +105,11 @@ export async function setAgentUri(options: SetAgentUriOptions): Promise<void> {
|
|
|
112
105
|
const resultData = printResult(receipt, timestamp, chainConfig.chain, chainConfig.chainId, hash);
|
|
113
106
|
|
|
114
107
|
if (options.outDir) {
|
|
115
|
-
writeResultJson(options.outDir, "
|
|
108
|
+
writeResultJson(options.outDir, "update", resultData);
|
|
116
109
|
}
|
|
117
110
|
}
|
|
118
111
|
|
|
119
|
-
interface
|
|
112
|
+
interface UpdateResult {
|
|
120
113
|
agentId?: string;
|
|
121
114
|
newUri?: string;
|
|
122
115
|
updatedBy?: `0x${string}`;
|
|
@@ -135,12 +128,12 @@ function printResult(
|
|
|
135
128
|
chain: Chain,
|
|
136
129
|
chainId: number,
|
|
137
130
|
hash: `0x${string}`,
|
|
138
|
-
):
|
|
131
|
+
): UpdateResult {
|
|
139
132
|
const events = parseEventLogs({ abi: IdentityRegistryAbi, logs: receipt.logs as Log[] });
|
|
140
133
|
const uriUpdated = events.find((e) => e.eventName === "URIUpdated");
|
|
141
134
|
|
|
142
135
|
const lines: string[] = [];
|
|
143
|
-
const result:
|
|
136
|
+
const result: UpdateResult = {
|
|
144
137
|
chainId,
|
|
145
138
|
block: receipt.blockNumber.toString(),
|
|
146
139
|
timestamp: new Date(Number(timestamp) * 1000).toUTCString(),
|
package/register/utils/chain.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { isAddress, type Chain } from "viem";
|
|
|
2
2
|
import { mainnet, sepolia, baseSepolia, foundry } from "viem/chains";
|
|
3
3
|
import { CHAIN_ID, getIdentityRegistryAddress } from "@aixyz/erc-8004";
|
|
4
4
|
import { select } from "@inquirer/prompts";
|
|
5
|
-
import { CliError } from "../utils";
|
|
6
5
|
|
|
7
6
|
export interface ChainConfig {
|
|
8
7
|
chain: Chain;
|
|
@@ -19,7 +18,7 @@ export const CHAINS: Record<string, ChainConfig> = {
|
|
|
19
18
|
export function resolveChainConfig(chainName: string): ChainConfig {
|
|
20
19
|
const config = CHAINS[chainName];
|
|
21
20
|
if (!config) {
|
|
22
|
-
throw new
|
|
21
|
+
throw new Error(`Unsupported chain: ${chainName}. Supported chains: ${Object.keys(CHAINS).join(", ")}`);
|
|
23
22
|
}
|
|
24
23
|
return config;
|
|
25
24
|
}
|
|
@@ -34,19 +33,19 @@ export async function selectChain(): Promise<string> {
|
|
|
34
33
|
export function resolveRegistryAddress(chainName: string, chainId: number, registry?: string): `0x${string}` {
|
|
35
34
|
if (registry) {
|
|
36
35
|
if (!isAddress(registry)) {
|
|
37
|
-
throw new
|
|
36
|
+
throw new Error(`Invalid registry address: ${registry}`);
|
|
38
37
|
}
|
|
39
38
|
return registry as `0x${string}`;
|
|
40
39
|
}
|
|
41
40
|
if (chainName === "localhost") {
|
|
42
|
-
throw new
|
|
41
|
+
throw new Error("--registry is required for localhost (no default contract deployment)");
|
|
43
42
|
}
|
|
44
43
|
return getIdentityRegistryAddress(chainId) as `0x${string}`;
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
export function validateBrowserRpcConflict(browser: boolean | undefined, rpcUrl: string | undefined): void {
|
|
48
47
|
if (browser && rpcUrl) {
|
|
49
|
-
throw new
|
|
48
|
+
throw new Error("--rpc-url cannot be used with browser wallet. The browser wallet uses its own RPC endpoint.");
|
|
50
49
|
}
|
|
51
50
|
}
|
|
52
51
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import type { RegistrationEntry } from "@aixyz/erc-8004/schemas/registration";
|
|
4
|
+
|
|
5
|
+
function getFilePath(cwd: string = process.cwd()): string {
|
|
6
|
+
return resolve(cwd, "app/erc-8004.ts");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function hasErc8004File(cwd?: string): boolean {
|
|
10
|
+
return existsSync(getFilePath(cwd));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createErc8004File(supportedTrust: string[], cwd?: string): void {
|
|
14
|
+
const filePath = getFilePath(cwd);
|
|
15
|
+
const trustArray = supportedTrust.map((t) => `"${t}"`).join(", ");
|
|
16
|
+
|
|
17
|
+
const content = `import type { ERC8004Registration } from "aixyz/erc-8004";
|
|
18
|
+
|
|
19
|
+
const metadata: ERC8004Registration = {
|
|
20
|
+
registrations: [],
|
|
21
|
+
supportedTrust: [${trustArray}],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default metadata;
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
writeFileSync(filePath, content, "utf-8");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function readRegistrations(cwd?: string): Promise<RegistrationEntry[]> {
|
|
31
|
+
const filePath = getFilePath(cwd);
|
|
32
|
+
|
|
33
|
+
if (!existsSync(filePath)) {
|
|
34
|
+
throw new Error(`No app/erc-8004.ts found. Run \`aixyz erc-8004 register\` first.`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const mod = await import(filePath);
|
|
38
|
+
const data = mod.default;
|
|
39
|
+
|
|
40
|
+
if (!data || !Array.isArray(data.registrations)) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return data.registrations;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function writeRegistrationEntry(entry: { agentId: number; agentRegistry: string }, cwd?: string): void {
|
|
48
|
+
const filePath = getFilePath(cwd);
|
|
49
|
+
const content = readFileSync(filePath, "utf-8");
|
|
50
|
+
const entryStr = `{ agentId: ${entry.agentId}, agentRegistry: "${entry.agentRegistry}" }`;
|
|
51
|
+
|
|
52
|
+
// Try to find `registrations: [...]` and insert the entry
|
|
53
|
+
const match = content.match(/registrations:\s*\[([^\]]*)\]/s);
|
|
54
|
+
if (match) {
|
|
55
|
+
const existing = match[1]!.trim();
|
|
56
|
+
const newEntries = existing ? `${existing}, ${entryStr}` : entryStr;
|
|
57
|
+
const updated = content.replace(/registrations:\s*\[([^\]]*)\]/s, `registrations: [${newEntries}]`);
|
|
58
|
+
writeFileSync(filePath, updated, "utf-8");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Fallback: append as comment
|
|
63
|
+
const comment = `\n// Registration added by \`aixyz erc-8004 register\`:\n// ${entryStr}\n`;
|
|
64
|
+
writeFileSync(filePath, content + comment, "utf-8");
|
|
65
|
+
}
|
package/register/utils/prompt.ts
CHANGED
|
@@ -1,18 +1,61 @@
|
|
|
1
|
-
import { input } from "@inquirer/prompts";
|
|
1
|
+
import { checkbox, confirm, input, select } from "@inquirer/prompts";
|
|
2
|
+
import type { RegistrationEntry } from "@aixyz/erc-8004/schemas/registration";
|
|
2
3
|
|
|
3
|
-
export async function
|
|
4
|
+
export async function promptAgentUrl(): Promise<string> {
|
|
4
5
|
return input({
|
|
5
|
-
message: "Agent
|
|
6
|
+
message: "Agent deployment URL (e.g., https://my-agent.example.com):",
|
|
6
7
|
validate: (value) => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(value);
|
|
10
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
11
|
+
return "URL must start with https:// or http://";
|
|
12
|
+
}
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return "Must be a valid URL (e.g., https://my-agent.example.com)";
|
|
16
|
+
}
|
|
10
17
|
},
|
|
11
18
|
});
|
|
12
19
|
}
|
|
13
20
|
|
|
14
|
-
export async function
|
|
15
|
-
return
|
|
16
|
-
message: "
|
|
21
|
+
export async function promptSupportedTrust(): Promise<string[]> {
|
|
22
|
+
return checkbox({
|
|
23
|
+
message: "Select supported trust mechanisms:",
|
|
24
|
+
choices: [
|
|
25
|
+
{ name: "reputation", value: "reputation", checked: true },
|
|
26
|
+
{ name: "crypto-economic", value: "crypto-economic" },
|
|
27
|
+
{ name: "tee-attestation", value: "tee-attestation" },
|
|
28
|
+
{ name: "social", value: "social" },
|
|
29
|
+
{ name: "governance", value: "governance" },
|
|
30
|
+
],
|
|
31
|
+
required: true,
|
|
17
32
|
});
|
|
18
33
|
}
|
|
34
|
+
|
|
35
|
+
export async function promptSelectRegistration(registrations: RegistrationEntry[]): Promise<RegistrationEntry> {
|
|
36
|
+
if (registrations.length === 1) {
|
|
37
|
+
const reg = registrations[0]!;
|
|
38
|
+
const yes = await confirm({
|
|
39
|
+
message: `Update this registration? (agentId: ${reg.agentId}, registry: ${reg.agentRegistry})`,
|
|
40
|
+
default: true,
|
|
41
|
+
});
|
|
42
|
+
if (!yes) {
|
|
43
|
+
throw new Error("Aborted.");
|
|
44
|
+
}
|
|
45
|
+
return reg;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return select({
|
|
49
|
+
message: "Select registration to update:",
|
|
50
|
+
choices: registrations.map((reg) => ({
|
|
51
|
+
name: `agentId: ${reg.agentId} — ${reg.agentRegistry}`,
|
|
52
|
+
value: reg,
|
|
53
|
+
})),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function deriveAgentUri(url: string): string {
|
|
58
|
+
// Ensure no trailing slash before appending path
|
|
59
|
+
const base = url.replace(/\/+$/, "");
|
|
60
|
+
return `${base}/_aixyz/erc-8004.json`;
|
|
61
|
+
}
|
|
@@ -103,4 +103,22 @@ describe("buildHtml", () => {
|
|
|
103
103
|
expect(html).not.toContain('<script>alert("xss")</script>');
|
|
104
104
|
expect(html).toContain("<script>");
|
|
105
105
|
});
|
|
106
|
+
|
|
107
|
+
test("shows 'Register Agent' by default (no mode)", () => {
|
|
108
|
+
const html = buildHtml(baseParams);
|
|
109
|
+
expect(html).toContain("Register Agent");
|
|
110
|
+
expect(html).not.toContain("Update Agent");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("shows 'Register Agent' when mode is 'register'", () => {
|
|
114
|
+
const html = buildHtml({ ...baseParams, mode: "register" });
|
|
115
|
+
expect(html).toContain("Register Agent");
|
|
116
|
+
expect(html).not.toContain("Update Agent");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("shows 'Update Agent' when mode is 'update'", () => {
|
|
120
|
+
const html = buildHtml({ ...baseParams, mode: "update" });
|
|
121
|
+
expect(html).toContain("Update Agent");
|
|
122
|
+
expect(html).not.toContain("Register Agent");
|
|
123
|
+
});
|
|
106
124
|
});
|
|
@@ -7,15 +7,16 @@ export interface BrowserSignParams {
|
|
|
7
7
|
chainName: string;
|
|
8
8
|
uri?: string;
|
|
9
9
|
gas?: bigint;
|
|
10
|
+
mode?: "register" | "update";
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
const TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
13
14
|
|
|
14
15
|
export async function signWithBrowser(params: BrowserSignParams): Promise<{ txHash: string }> {
|
|
15
|
-
const { registryAddress, calldata, chainId, chainName, uri, gas } = params;
|
|
16
|
+
const { registryAddress, calldata, chainId, chainName, uri, gas, mode } = params;
|
|
16
17
|
|
|
17
18
|
const nonce = crypto.randomUUID();
|
|
18
|
-
const html = buildHtml({ registryAddress, calldata, chainId, chainName, uri, gas, nonce });
|
|
19
|
+
const html = buildHtml({ registryAddress, calldata, chainId, chainName, uri, gas, nonce, mode });
|
|
19
20
|
|
|
20
21
|
const { promise: resultPromise, resolve, reject } = Promise.withResolvers<{ txHash: string }>();
|
|
21
22
|
let settled = false;
|
|
@@ -134,8 +135,11 @@ export function buildHtml(params: {
|
|
|
134
135
|
uri?: string;
|
|
135
136
|
gas?: bigint;
|
|
136
137
|
nonce: string;
|
|
138
|
+
mode?: "register" | "update";
|
|
137
139
|
}): string {
|
|
138
|
-
const { registryAddress, calldata, chainId, chainName, uri, gas, nonce } = params;
|
|
140
|
+
const { registryAddress, calldata, chainId, chainName, uri, gas, nonce, mode } = params;
|
|
141
|
+
const isUpdate = mode === "update";
|
|
142
|
+
const actionLabel = isUpdate ? "Update Agent" : "Register Agent";
|
|
139
143
|
const chainIdHex = `0x${chainId.toString(16)}`;
|
|
140
144
|
|
|
141
145
|
const displayUri = uri && uri.length > 80 ? uri.slice(0, 80) + "..." : (uri ?? "");
|
|
@@ -145,7 +149,7 @@ export function buildHtml(params: {
|
|
|
145
149
|
<head>
|
|
146
150
|
<meta charset="UTF-8">
|
|
147
151
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
148
|
-
<title>
|
|
152
|
+
<title>aixyz.sh – ERC-8004 ${actionLabel}</title>
|
|
149
153
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
150
154
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
151
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">
|
|
@@ -475,8 +479,8 @@ export function buildHtml(params: {
|
|
|
475
479
|
<body>
|
|
476
480
|
<div class="container">
|
|
477
481
|
<div class="header">
|
|
478
|
-
<div class="brand">
|
|
479
|
-
<h1
|
|
482
|
+
<div class="brand">aixyz erc-8004</div>
|
|
483
|
+
<h1>${actionLabel}</h1>
|
|
480
484
|
</div>
|
|
481
485
|
|
|
482
486
|
<div class="details" id="details">
|
|
@@ -513,7 +517,7 @@ export function buildHtml(params: {
|
|
|
513
517
|
<div id="walletList"></div>
|
|
514
518
|
</div>
|
|
515
519
|
|
|
516
|
-
<button id="registerBtn" disabled
|
|
520
|
+
<button id="registerBtn" disabled>${actionLabel}</button>
|
|
517
521
|
|
|
518
522
|
<div class="status" id="status"></div>
|
|
519
523
|
</div>
|
|
@@ -524,6 +528,7 @@ export function buildHtml(params: {
|
|
|
524
528
|
const CHAIN_ID_HEX = ${safeJsonEmbed(chainIdHex)};
|
|
525
529
|
const CHAIN_ID = ${chainId};
|
|
526
530
|
const GAS = ${gas ? safeJsonEmbed(`0x${gas.toString(16)}`) : "undefined"};
|
|
531
|
+
const ACTION_LABEL = ${safeJsonEmbed(actionLabel)};
|
|
527
532
|
|
|
528
533
|
const registerBtn = document.getElementById("registerBtn");
|
|
529
534
|
const statusEl = document.getElementById("status");
|
|
@@ -543,7 +548,7 @@ export function buildHtml(params: {
|
|
|
543
548
|
walletInfo.style.display = "none";
|
|
544
549
|
registerBtn.style.display = "none";
|
|
545
550
|
registerBtn.disabled = true;
|
|
546
|
-
registerBtn.textContent =
|
|
551
|
+
registerBtn.textContent = ACTION_LABEL;
|
|
547
552
|
walletSectionEl.style.display = "";
|
|
548
553
|
statusEl.className = "status";
|
|
549
554
|
if (discoveredWallets.size > 0) {
|
|
@@ -663,7 +668,7 @@ export function buildHtml(params: {
|
|
|
663
668
|
walletSectionEl.style.display = "none";
|
|
664
669
|
registerBtn.style.display = "block";
|
|
665
670
|
registerBtn.disabled = false;
|
|
666
|
-
setStatus("Wallet connected. Ready to register.", "success");
|
|
671
|
+
setStatus("Wallet connected. Ready to ${isUpdate ? "update" : "register"}.", "success");
|
|
667
672
|
|
|
668
673
|
// Listen for account/chain changes on the selected provider
|
|
669
674
|
if (selectedProvider.on) {
|
|
@@ -739,7 +744,7 @@ export function buildHtml(params: {
|
|
|
739
744
|
setStatus("Failed: " + err.message + " — You can try again.", "error");
|
|
740
745
|
}
|
|
741
746
|
registerBtn.disabled = false;
|
|
742
|
-
registerBtn.textContent =
|
|
747
|
+
registerBtn.textContent = ACTION_LABEL;
|
|
743
748
|
}
|
|
744
749
|
});
|
|
745
750
|
</script>
|
package/register/wallet/index.ts
CHANGED
|
@@ -3,7 +3,6 @@ import type { Chain, WalletClient } from "viem";
|
|
|
3
3
|
import { select, input, password } from "@inquirer/prompts";
|
|
4
4
|
import { createPrivateKeyWallet } from "./privatekey";
|
|
5
5
|
import { createKeystoreWallet } from "./keystore";
|
|
6
|
-
import { CliError } from "../utils";
|
|
7
6
|
|
|
8
7
|
export interface WalletOptions {
|
|
9
8
|
keystore?: string;
|
|
@@ -62,7 +61,7 @@ export async function selectWalletMethod(options: WalletOptions): Promise<Wallet
|
|
|
62
61
|
return { type: "privatekey", resolveKey: () => Promise.resolve(key) };
|
|
63
62
|
}
|
|
64
63
|
default:
|
|
65
|
-
throw new
|
|
64
|
+
throw new Error("No wallet method selected");
|
|
66
65
|
}
|
|
67
66
|
}
|
|
68
67
|
|
|
@@ -77,7 +76,7 @@ export async function createWalletFromMethod(
|
|
|
77
76
|
case "keystore":
|
|
78
77
|
return createKeystoreWallet(method.path, chain, rpcUrl);
|
|
79
78
|
case "browser":
|
|
80
|
-
throw new
|
|
79
|
+
throw new Error("Browser wallets should use registerWithBrowser, not createWalletFromMethod");
|
|
81
80
|
}
|
|
82
81
|
}
|
|
83
82
|
|
|
@@ -9,7 +9,7 @@ const TEST_PRIVATE_KEY = "0x0000000000000000000000000000000000000000000000000000
|
|
|
9
9
|
const TEST_ADDRESS = "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf";
|
|
10
10
|
const TEST_PASSWORD = "testpassword";
|
|
11
11
|
|
|
12
|
-
const testDir = join(tmpdir(), "
|
|
12
|
+
const testDir = join(tmpdir(), "aixyz-cli-keystore-test");
|
|
13
13
|
const testKeystorePath = join(testDir, "test-keystore.json");
|
|
14
14
|
|
|
15
15
|
// Mock the password prompt to return TEST_PASSWORD
|
|
@@ -3,17 +3,16 @@ import { decryptKeystoreJson, isKeystoreJson } from "ethers";
|
|
|
3
3
|
import { createWalletClient, http, type Chain, type WalletClient } from "viem";
|
|
4
4
|
import { privateKeyToAccount } from "viem/accounts";
|
|
5
5
|
import { password } from "@inquirer/prompts";
|
|
6
|
-
import { CliError } from "../utils";
|
|
7
6
|
|
|
8
7
|
export async function decryptKeystore(keystorePath: string): Promise<`0x${string}`> {
|
|
9
8
|
const file = Bun.file(keystorePath);
|
|
10
9
|
if (!(await file.exists())) {
|
|
11
|
-
throw new
|
|
10
|
+
throw new Error(`Keystore file not found: ${keystorePath}`);
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
const json = await file.text();
|
|
15
14
|
if (!isKeystoreJson(json)) {
|
|
16
|
-
throw new
|
|
15
|
+
throw new Error(`Invalid keystore file: ${keystorePath}`);
|
|
17
16
|
}
|
|
18
17
|
const pass = await password({ message: "Enter keystore password:", mask: "*" });
|
|
19
18
|
const account = await decryptKeystoreJson(json, pass);
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { createWalletClient, http, type Chain, type WalletClient } from "viem";
|
|
2
2
|
import { privateKeyToAccount } from "viem/accounts";
|
|
3
|
-
import { validatePrivateKey } from "../utils";
|
|
4
3
|
|
|
5
4
|
export function createPrivateKeyWallet(privateKey: string, chain: Chain, rpcUrl?: string): WalletClient {
|
|
6
|
-
const key =
|
|
7
|
-
|
|
5
|
+
const key = (privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`) as `0x${string}`;
|
|
6
|
+
|
|
7
|
+
let account;
|
|
8
|
+
try {
|
|
9
|
+
account = privateKeyToAccount(key);
|
|
10
|
+
} catch {
|
|
11
|
+
throw new Error("Invalid private key format. Expected 64 hex characters (with or without 0x prefix).");
|
|
12
|
+
}
|
|
8
13
|
|
|
9
14
|
return createWalletClient({
|
|
10
15
|
account,
|
package/register/wallet/sign.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Chain } from "viem";
|
|
2
2
|
import { signWithBrowser } from "./browser";
|
|
3
3
|
import { createWalletFromMethod, type WalletMethod } from "./index";
|
|
4
|
-
import { CliError } from "../utils";
|
|
5
4
|
|
|
6
5
|
export interface TxRequest {
|
|
7
6
|
to: `0x${string}`;
|
|
@@ -10,7 +9,7 @@ export interface TxRequest {
|
|
|
10
9
|
}
|
|
11
10
|
|
|
12
11
|
export interface SignOptions {
|
|
13
|
-
browser?: { chainId: number; chainName: string; uri?: string };
|
|
12
|
+
browser?: { chainId: number; chainName: string; uri?: string; mode?: "register" | "update" };
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
export type SignTransactionResult =
|
|
@@ -35,13 +34,14 @@ export async function signTransaction({
|
|
|
35
34
|
switch (walletMethod.type) {
|
|
36
35
|
case "browser": {
|
|
37
36
|
if (!options?.browser) {
|
|
38
|
-
throw new
|
|
37
|
+
throw new Error("Browser wallet requires chainId and chainName parameters");
|
|
39
38
|
}
|
|
40
39
|
return signViaBrowser({
|
|
41
40
|
tx,
|
|
42
41
|
chainId: options.browser.chainId,
|
|
43
42
|
chainName: options.browser.chainName,
|
|
44
43
|
uri: options.browser.uri,
|
|
44
|
+
mode: options.browser.mode,
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
default: {
|
|
@@ -56,11 +56,13 @@ async function signViaBrowser({
|
|
|
56
56
|
chainId,
|
|
57
57
|
chainName,
|
|
58
58
|
uri,
|
|
59
|
+
mode,
|
|
59
60
|
}: {
|
|
60
61
|
tx: TxRequest;
|
|
61
62
|
chainId: number;
|
|
62
63
|
chainName: string;
|
|
63
64
|
uri?: string;
|
|
65
|
+
mode?: "register" | "update";
|
|
64
66
|
}): Promise<SignTransactionResult> {
|
|
65
67
|
const { txHash } = await signWithBrowser({
|
|
66
68
|
registryAddress: tx.to,
|
|
@@ -69,10 +71,11 @@ async function signViaBrowser({
|
|
|
69
71
|
chainName,
|
|
70
72
|
uri,
|
|
71
73
|
gas: tx.gas,
|
|
74
|
+
mode,
|
|
72
75
|
});
|
|
73
76
|
|
|
74
77
|
if (typeof txHash !== "string" || !/^0x[0-9a-f]{64}$/i.test(txHash)) {
|
|
75
|
-
throw new
|
|
78
|
+
throw new Error(`Invalid transaction hash received from browser wallet: ${txHash}`);
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
return { kind: "sent", txHash: txHash as `0x${string}` };
|
|
@@ -93,7 +96,7 @@ async function signWithWalletClient({
|
|
|
93
96
|
|
|
94
97
|
const account = walletClient.account;
|
|
95
98
|
if (!account) {
|
|
96
|
-
throw new
|
|
99
|
+
throw new Error("Wallet client does not have an account configured");
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
const request = await walletClient.prepareTransactionRequest({
|