@aixyz/cli 0.8.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.
- package/bin.ts +43 -81
- package/build/AixyzConfigPlugin.ts +1 -1
- package/build/AixyzServerPlugin.ts +10 -0
- package/package.json +3 -3
- package/register/register.test.ts +0 -9
- package/register/register.ts +39 -13
- package/register/update.test.ts +47 -0
- package/register/{set-agent-uri.ts → update.ts} +39 -40
- package/register/utils/chain.ts +4 -5
- package/register/utils/erc8004-file.ts +65 -0
- package/register/utils/prompt.ts +61 -1
- package/register/utils.test.ts +4 -77
- package/register/utils.ts +3 -20
- 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
|
@@ -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,4 +1,5 @@
|
|
|
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
4
|
export async function promptAgentId(): Promise<string> {
|
|
4
5
|
return input({
|
|
@@ -16,3 +17,62 @@ export async function promptUri(): Promise<string> {
|
|
|
16
17
|
message: "New agent metadata URI or path to .json file (leave empty to clear):",
|
|
17
18
|
});
|
|
18
19
|
}
|
|
20
|
+
|
|
21
|
+
export async function promptAgentUrl(): Promise<string> {
|
|
22
|
+
return input({
|
|
23
|
+
message: "Agent deployment URL (e.g., https://my-agent.example.com):",
|
|
24
|
+
validate: (value) => {
|
|
25
|
+
try {
|
|
26
|
+
const url = new URL(value);
|
|
27
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
28
|
+
return "URL must start with https:// or http://";
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
} catch {
|
|
32
|
+
return "Must be a valid URL (e.g., https://my-agent.example.com)";
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function promptSupportedTrust(): Promise<string[]> {
|
|
39
|
+
return checkbox({
|
|
40
|
+
message: "Select supported trust mechanisms:",
|
|
41
|
+
choices: [
|
|
42
|
+
{ name: "reputation", value: "reputation", checked: true },
|
|
43
|
+
{ name: "crypto-economic", value: "crypto-economic" },
|
|
44
|
+
{ name: "tee-attestation", value: "tee-attestation" },
|
|
45
|
+
{ name: "social", value: "social" },
|
|
46
|
+
{ name: "governance", value: "governance" },
|
|
47
|
+
],
|
|
48
|
+
required: true,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function promptSelectRegistration(registrations: RegistrationEntry[]): Promise<RegistrationEntry> {
|
|
53
|
+
if (registrations.length === 1) {
|
|
54
|
+
const reg = registrations[0]!;
|
|
55
|
+
const yes = await confirm({
|
|
56
|
+
message: `Update this registration? (agentId: ${reg.agentId}, registry: ${reg.agentRegistry})`,
|
|
57
|
+
default: true,
|
|
58
|
+
});
|
|
59
|
+
if (!yes) {
|
|
60
|
+
throw new Error("Aborted.");
|
|
61
|
+
}
|
|
62
|
+
return reg;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return select({
|
|
66
|
+
message: "Select registration to update:",
|
|
67
|
+
choices: registrations.map((reg) => ({
|
|
68
|
+
name: `agentId: ${reg.agentId} — ${reg.agentRegistry}`,
|
|
69
|
+
value: reg,
|
|
70
|
+
})),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function deriveAgentUri(url: string): string {
|
|
75
|
+
// Ensure no trailing slash before appending path
|
|
76
|
+
const base = url.replace(/\/+$/, "");
|
|
77
|
+
return `${base}/_aixyz/erc-8004.json`;
|
|
78
|
+
}
|
package/register/utils.test.ts
CHANGED
|
@@ -1,82 +1,9 @@
|
|
|
1
1
|
import { describe, expect, test, afterAll, beforeAll } from "bun:test";
|
|
2
2
|
import { rmSync } from "fs";
|
|
3
3
|
import { mkdir } from "node:fs/promises";
|
|
4
|
-
import {
|
|
4
|
+
import { resolveUri } from "./utils";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
|
|
7
|
-
describe("validatePrivateKey", () => {
|
|
8
|
-
test("accepts valid 64-char hex key with 0x prefix", () => {
|
|
9
|
-
const key = "0x0000000000000000000000000000000000000000000000000000000000000001";
|
|
10
|
-
const result = validatePrivateKey(key);
|
|
11
|
-
expect(result).toStrictEqual(key);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test("accepts valid 64-char hex key without 0x prefix", () => {
|
|
15
|
-
const key = "0000000000000000000000000000000000000000000000000000000000000001";
|
|
16
|
-
const result = validatePrivateKey(key);
|
|
17
|
-
expect(result).toStrictEqual(`0x${key}`);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test("accepts mixed case hex characters", () => {
|
|
21
|
-
const key = "0xaAbBcCdDeEfF0000000000000000000000000000000000000000000000000001";
|
|
22
|
-
const result = validatePrivateKey(key);
|
|
23
|
-
expect(result).toStrictEqual(key);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("rejects key that is too short", () => {
|
|
27
|
-
const key = "0x1234";
|
|
28
|
-
expect(() => validatePrivateKey(key)).toThrow(CliError);
|
|
29
|
-
expect(() => validatePrivateKey(key)).toThrow("Invalid private key format");
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("rejects key that is too long", () => {
|
|
33
|
-
const key = "0x00000000000000000000000000000000000000000000000000000000000000001";
|
|
34
|
-
expect(() => validatePrivateKey(key)).toThrow(CliError);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test("rejects key with invalid characters", () => {
|
|
38
|
-
const key = "0xGGGG000000000000000000000000000000000000000000000000000000000001";
|
|
39
|
-
expect(() => validatePrivateKey(key)).toThrow(CliError);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("rejects empty string", () => {
|
|
43
|
-
expect(() => validatePrivateKey("")).toThrow(CliError);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("rejects random string", () => {
|
|
47
|
-
expect(() => validatePrivateKey("not-a-key")).toThrow(CliError);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe("CliError", () => {
|
|
52
|
-
test("is an instance of Error", () => {
|
|
53
|
-
const error = new CliError("test message");
|
|
54
|
-
expect(error).toBeInstanceOf(Error);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("has correct name property", () => {
|
|
58
|
-
const error = new CliError("test message");
|
|
59
|
-
expect(error.name).toStrictEqual("CliError");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("has correct message property", () => {
|
|
63
|
-
const error = new CliError("test message");
|
|
64
|
-
expect(error.message).toStrictEqual("test message");
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("can be caught as Error", () => {
|
|
68
|
-
let caught = false;
|
|
69
|
-
try {
|
|
70
|
-
throw new CliError("test");
|
|
71
|
-
} catch (e) {
|
|
72
|
-
if (e instanceof Error) {
|
|
73
|
-
caught = true;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
expect(caught).toStrictEqual(true);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
7
|
describe("resolveUri", () => {
|
|
81
8
|
const testDir = join(import.meta.dir, "__test_fixtures__");
|
|
82
9
|
const testJsonPath = join(testDir, "test-metadata.json");
|
|
@@ -129,7 +56,7 @@ describe("resolveUri", () => {
|
|
|
129
56
|
await mkdir(dirWithJsonSuffix, { recursive: true });
|
|
130
57
|
|
|
131
58
|
try {
|
|
132
|
-
expect(() => resolveUri(dirWithJsonSuffix)).toThrow(
|
|
59
|
+
expect(() => resolveUri(dirWithJsonSuffix)).toThrow(Error);
|
|
133
60
|
expect(() => resolveUri(dirWithJsonSuffix)).toThrow("Not a file");
|
|
134
61
|
} finally {
|
|
135
62
|
rmSync(dirWithJsonSuffix, { recursive: true, force: true });
|
|
@@ -137,7 +64,7 @@ describe("resolveUri", () => {
|
|
|
137
64
|
});
|
|
138
65
|
|
|
139
66
|
test("throws for non-existent .json file", () => {
|
|
140
|
-
expect(() => resolveUri("./non-existent.json")).toThrow(
|
|
67
|
+
expect(() => resolveUri("./non-existent.json")).toThrow(Error);
|
|
141
68
|
expect(() => resolveUri("./non-existent.json")).toThrow("File not found");
|
|
142
69
|
});
|
|
143
70
|
|
|
@@ -145,7 +72,7 @@ describe("resolveUri", () => {
|
|
|
145
72
|
await Bun.write(testJsonPath, "not valid json {{{");
|
|
146
73
|
|
|
147
74
|
try {
|
|
148
|
-
expect(() => resolveUri(testJsonPath)).toThrow(
|
|
75
|
+
expect(() => resolveUri(testJsonPath)).toThrow(Error);
|
|
149
76
|
expect(() => resolveUri(testJsonPath)).toThrow("Invalid JSON");
|
|
150
77
|
} finally {
|
|
151
78
|
await Bun.file(testJsonPath).unlink();
|
package/register/utils.ts
CHANGED
|
@@ -12,11 +12,11 @@ export function resolveUri(uri: string): string {
|
|
|
12
12
|
const filePath = resolve(uri);
|
|
13
13
|
|
|
14
14
|
if (!existsSync(filePath)) {
|
|
15
|
-
throw new
|
|
15
|
+
throw new Error(`File not found: ${filePath}`);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
if (!statSync(filePath).isFile()) {
|
|
19
|
-
throw new
|
|
19
|
+
throw new Error(`Not a file: ${filePath}`);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const content = readFileSync(filePath, "utf-8");
|
|
@@ -25,7 +25,7 @@ export function resolveUri(uri: string): string {
|
|
|
25
25
|
try {
|
|
26
26
|
JSON.parse(content);
|
|
27
27
|
} catch {
|
|
28
|
-
throw new
|
|
28
|
+
throw new Error(`Invalid JSON in file: ${filePath}`);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// Convert to base64 data URI
|
|
@@ -36,20 +36,3 @@ export function resolveUri(uri: string): string {
|
|
|
36
36
|
// Return as-is for other URIs
|
|
37
37
|
return uri;
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
export function validatePrivateKey(key: string): `0x${string}` {
|
|
41
|
-
const normalizedKey = key.startsWith("0x") ? key : `0x${key}`;
|
|
42
|
-
|
|
43
|
-
if (!/^0x[0-9a-fA-F]{64}$/.test(normalizedKey)) {
|
|
44
|
-
throw new CliError("Invalid private key format. Expected 64 hex characters (with or without 0x prefix).");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return normalizedKey as `0x${string}`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export class CliError extends Error {
|
|
51
|
-
constructor(message: string) {
|
|
52
|
-
super(message);
|
|
53
|
-
this.name = "CliError";
|
|
54
|
-
}
|
|
55
|
-
}
|
|
@@ -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({
|
package/register/README.md
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
# ERC-8004 Registry Commands
|
|
2
|
-
|
|
3
|
-
CLI commands for registering agents to the [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) IdentityRegistry.
|
|
4
|
-
|
|
5
|
-
These commands are part of the `aixyz` CLI under the `erc8004` subcommand.
|
|
6
|
-
|
|
7
|
-
## Usage
|
|
8
|
-
|
|
9
|
-
### Register an Agent
|
|
10
|
-
|
|
11
|
-
Register a new agent to the IdentityRegistry with multiple wallet options:
|
|
12
|
-
|
|
13
|
-
#### Using Keystore (Recommended)
|
|
14
|
-
|
|
15
|
-
Sign with an Ethereum keystore (V3) JSON file:
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
aixyz erc8004 register --uri "./metadata.json" --chain sepolia --keystore ~/.foundry/keystores/default --broadcast
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
#### Using Browser Wallet
|
|
22
|
-
|
|
23
|
-
Opens a localhost page to sign with any browser extension wallet (MetaMask, Rabby, etc.) that are `EIP-6963` compliant:
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
aixyz erc8004 register --uri "ipfs://Qm..." --chain sepolia --browser --broadcast
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
> **Note:** `--rpc-url` cannot be used with `--browser`. The browser wallet uses its own RPC endpoint.
|
|
30
|
-
|
|
31
|
-
#### Using Private Key Env (Not Recommended)
|
|
32
|
-
|
|
33
|
-
For scripting and CI:
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
# Not recommended for interactive use
|
|
37
|
-
PRIVATE_KEY=0x... aixyz erc8004 register --uri "ipfs://Qm..." --chain sepolia --broadcast
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
#### Interactive Mode
|
|
41
|
-
|
|
42
|
-
If no wallet option is provided, you'll be prompted to choose:
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
aixyz erc8004 register --uri "ipfs://Qm..." --chain sepolia --broadcast
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
#### Local Development
|
|
49
|
-
|
|
50
|
-
Register against a local Foundry/Anvil node:
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
aixyz erc8004 register \
|
|
54
|
-
--chain localhost \
|
|
55
|
-
--registry 0x5FbDB2315678afecb367f032d93F642f64180aa3 \
|
|
56
|
-
--rpc-url http://localhost:8545 \
|
|
57
|
-
--uri "./metadata.json" \
|
|
58
|
-
--keystore ~/.foundry/keystores/default \
|
|
59
|
-
--broadcast
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### Set Agent URI
|
|
63
|
-
|
|
64
|
-
Update the metadata URI of a registered agent:
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
aixyz erc8004 set-agent-uri \
|
|
68
|
-
--agent-id 1 \
|
|
69
|
-
--uri "https://my-agent.vercel.app/.well-known/agent-card.json" \
|
|
70
|
-
--chain sepolia \
|
|
71
|
-
--keystore ~/.foundry/keystores/default \
|
|
72
|
-
--broadcast
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### Options
|
|
76
|
-
|
|
77
|
-
| Option | Description |
|
|
78
|
-
| ---------------------- | ------------------------------------------------------------------------------------ |
|
|
79
|
-
| `--uri <uri>` | Agent metadata URI or path to `.json` file (converts to base64 data URI) |
|
|
80
|
-
| `--chain <chain>` | Target chain: `mainnet`, `sepolia`, `base-sepolia`, `localhost` (default: `sepolia`) |
|
|
81
|
-
| `--rpc-url <url>` | Custom RPC URL (cannot be used with `--browser`) |
|
|
82
|
-
| `--registry <address>` | IdentityRegistry contract address (required for `localhost`) |
|
|
83
|
-
| `--keystore <path>` | Path to Ethereum keystore (V3) JSON file |
|
|
84
|
-
| `--browser` | Use browser extension wallet |
|
|
85
|
-
| `--broadcast` | Sign and broadcast the transaction (default: dry-run) |
|
|
86
|
-
| `--out-dir <path>` | Write deployment result as JSON to the given directory |
|
|
87
|
-
|
|
88
|
-
### Environment Variables
|
|
89
|
-
|
|
90
|
-
| Variable | Description |
|
|
91
|
-
| ------------- | ------------------------------------- |
|
|
92
|
-
| `PRIVATE_KEY` | Private key for signing (use caution) |
|
|
93
|
-
|
|
94
|
-
### Supported Chains
|
|
95
|
-
|
|
96
|
-
| Chain | Chain ID | Network |
|
|
97
|
-
| -------------- | -------- | ------------------------ |
|
|
98
|
-
| `mainnet` | 1 | Ethereum mainnet |
|
|
99
|
-
| `sepolia` | 11155111 | Ethereum Sepolia testnet |
|
|
100
|
-
| `base-sepolia` | 84532 | Base Sepolia testnet |
|
|
101
|
-
| `localhost` | 31337 | Local Foundry/Anvil node |
|