@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.
- package/README.md +1 -1
- package/bin.ts +137 -0
- package/build/AixyzConfigPlugin.ts +1 -1
- package/build/AixyzServerPlugin.ts +43 -9
- package/build/index.ts +1 -1
- package/dev/index.ts +1 -1
- package/package.json +11 -6
- package/register/index.ts +8 -0
- package/register/register.test.ts +66 -0
- package/register/register.ts +204 -0
- package/register/update.test.ts +47 -0
- package/register/update.ts +191 -0
- package/register/utils/chain.ts +56 -0
- package/register/utils/erc8004-file.ts +65 -0
- package/register/utils/prompt.ts +78 -0
- package/register/utils/result.ts +14 -0
- package/register/utils/spinner.ts +39 -0
- package/register/utils/transaction.ts +94 -0
- package/register/utils.test.ts +81 -0
- package/register/utils.ts +38 -0
- package/register/wallet/browser.test.ts +124 -0
- package/register/wallet/browser.ts +753 -0
- package/register/wallet/index.test.ts +80 -0
- package/register/wallet/index.ts +83 -0
- package/register/wallet/keystore.test.ts +63 -0
- package/register/wallet/keystore.ts +32 -0
- package/register/wallet/privatekey.test.ts +30 -0
- package/register/wallet/privatekey.ts +19 -0
- package/register/wallet/sign.test.ts +78 -0
- package/register/wallet/sign.ts +112 -0
package/README.md
CHANGED
package/bin.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { program } from "commander";
|
|
3
3
|
import { build } from "./build";
|
|
4
4
|
import { dev } from "./dev";
|
|
5
|
+
import { register } from "./register/register";
|
|
6
|
+
import { update } from "./register/update";
|
|
5
7
|
import pkg from "./package.json";
|
|
6
8
|
|
|
7
9
|
function handleAction(
|
|
@@ -11,6 +13,9 @@ function handleAction(
|
|
|
11
13
|
try {
|
|
12
14
|
await action(options);
|
|
13
15
|
} catch (error) {
|
|
16
|
+
if (error instanceof Error && error.name === "ExitPromptError") {
|
|
17
|
+
process.exit(130);
|
|
18
|
+
}
|
|
14
19
|
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
15
20
|
process.exit(1);
|
|
16
21
|
}
|
|
@@ -60,4 +65,136 @@ Examples:
|
|
|
60
65
|
)
|
|
61
66
|
.action(handleAction(build));
|
|
62
67
|
|
|
68
|
+
const erc8004 = program.command("erc-8004").description("ERC-8004 IdentityRegistry operations");
|
|
69
|
+
|
|
70
|
+
erc8004
|
|
71
|
+
.command("register")
|
|
72
|
+
.description("Register a new agent to the ERC-8004 IdentityRegistry")
|
|
73
|
+
.option("--url <url>", "Agent deployment URL (e.g., https://my-agent.example.com)")
|
|
74
|
+
.option("--chain <chain>", "Target chain (mainnet, sepolia, base-sepolia, localhost)")
|
|
75
|
+
.option("--rpc-url <url>", "Custom RPC URL (uses default if not provided)")
|
|
76
|
+
.option("--registry <address>", "Contract address of the IdentityRegistry (required for localhost)")
|
|
77
|
+
.option("--keystore <path>", "Path to Ethereum keystore (V3) JSON file for local signing")
|
|
78
|
+
.option("--browser", "Use browser extension wallet (any extension)")
|
|
79
|
+
.option("--broadcast", "Sign and broadcast the transaction (default: dry-run)")
|
|
80
|
+
.option("--out-dir <path>", "Write deployment result as JSON to the given directory")
|
|
81
|
+
.addHelpText(
|
|
82
|
+
"after",
|
|
83
|
+
`
|
|
84
|
+
Option Details:
|
|
85
|
+
--url <url>
|
|
86
|
+
Agent deployment URL (e.g., https://my-agent.example.com).
|
|
87
|
+
The registration URI will be derived as <url>/_aixyz/erc-8004.json.
|
|
88
|
+
If omitted, you will be prompted to enter the URL interactively.
|
|
89
|
+
|
|
90
|
+
--chain <chain>
|
|
91
|
+
Target chain for registration. Supported values:
|
|
92
|
+
mainnet Ethereum mainnet (chain ID 1)
|
|
93
|
+
sepolia Ethereum Sepolia testnet (chain ID 11155111)
|
|
94
|
+
base-sepolia Base Sepolia testnet (chain ID 84532)
|
|
95
|
+
localhost Local Foundry/Anvil node (chain ID 31337)
|
|
96
|
+
If omitted, you will be prompted to select a chain interactively.
|
|
97
|
+
|
|
98
|
+
--rpc-url <url>
|
|
99
|
+
Custom RPC endpoint URL. Overrides the default RPC for the selected
|
|
100
|
+
chain. Cannot be used with --browser since the browser wallet manages
|
|
101
|
+
its own RPC connection.
|
|
102
|
+
|
|
103
|
+
--registry <address>
|
|
104
|
+
Contract address of the ERC-8004 IdentityRegistry. Only required for
|
|
105
|
+
localhost, where there is no default deployment.
|
|
106
|
+
|
|
107
|
+
--keystore <path>
|
|
108
|
+
Path to an Ethereum keystore (V3) JSON file. You will be prompted for
|
|
109
|
+
the keystore password to decrypt the private key for signing.
|
|
110
|
+
|
|
111
|
+
--browser
|
|
112
|
+
Opens a local page in your default browser for signing with any
|
|
113
|
+
EIP-6963 compatible wallet extension (MetaMask, Rabby, etc.).
|
|
114
|
+
|
|
115
|
+
--broadcast
|
|
116
|
+
Sign and broadcast the transaction on-chain. Without this flag the
|
|
117
|
+
command performs a dry-run.
|
|
118
|
+
|
|
119
|
+
--out-dir <path>
|
|
120
|
+
Directory to write the deployment result as a JSON file.
|
|
121
|
+
|
|
122
|
+
Behavior:
|
|
123
|
+
If app/erc-8004.ts does not exist, you will be prompted to create it
|
|
124
|
+
(selecting supported trust mechanisms). After a successful on-chain
|
|
125
|
+
registration, the new registration entry is written back to app/erc-8004.ts.
|
|
126
|
+
|
|
127
|
+
Environment Variables:
|
|
128
|
+
PRIVATE_KEY Private key (hex, with or without 0x prefix) used for signing.
|
|
129
|
+
|
|
130
|
+
Examples:
|
|
131
|
+
# Dry-run (default)
|
|
132
|
+
$ aixyz erc-8004 register --url "https://my-agent.example.com" --chain sepolia
|
|
133
|
+
|
|
134
|
+
# Sign and broadcast
|
|
135
|
+
$ aixyz erc-8004 register --url "https://my-agent.example.com" --chain sepolia --keystore ~/.foundry/keystores/default --broadcast
|
|
136
|
+
$ aixyz erc-8004 register --url "https://my-agent.example.com" --chain sepolia --browser --broadcast`,
|
|
137
|
+
)
|
|
138
|
+
.action(handleAction(register));
|
|
139
|
+
|
|
140
|
+
erc8004
|
|
141
|
+
.command("update")
|
|
142
|
+
.description("Update the metadata URI of a registered agent")
|
|
143
|
+
.option("--url <url>", "New agent deployment URL (e.g., https://my-agent.example.com)")
|
|
144
|
+
.option("--rpc-url <url>", "Custom RPC URL (uses default if not provided)")
|
|
145
|
+
.option("--registry <address>", "Contract address of the IdentityRegistry (required for localhost)")
|
|
146
|
+
.option("--keystore <path>", "Path to Ethereum keystore (V3) JSON file for local signing")
|
|
147
|
+
.option("--browser", "Use browser extension wallet (any extension)")
|
|
148
|
+
.option("--broadcast", "Sign and broadcast the transaction (default: dry-run)")
|
|
149
|
+
.option("--out-dir <path>", "Write result as JSON to the given directory")
|
|
150
|
+
.addHelpText(
|
|
151
|
+
"after",
|
|
152
|
+
`
|
|
153
|
+
Option Details:
|
|
154
|
+
--url <url>
|
|
155
|
+
New agent deployment URL (e.g., https://my-agent.example.com).
|
|
156
|
+
The URI will be derived as <url>/_aixyz/erc-8004.json.
|
|
157
|
+
If omitted, you will be prompted to enter the URL interactively.
|
|
158
|
+
|
|
159
|
+
--rpc-url <url>
|
|
160
|
+
Custom RPC endpoint URL. Overrides the default RPC for the selected
|
|
161
|
+
chain. Cannot be used with --browser.
|
|
162
|
+
|
|
163
|
+
--registry <address>
|
|
164
|
+
Contract address of the ERC-8004 IdentityRegistry. Only required for
|
|
165
|
+
localhost, where there is no default deployment.
|
|
166
|
+
|
|
167
|
+
--keystore <path>
|
|
168
|
+
Path to an Ethereum keystore (V3) JSON file.
|
|
169
|
+
|
|
170
|
+
--browser
|
|
171
|
+
Opens a local page in your default browser for signing with any
|
|
172
|
+
EIP-6963 compatible wallet extension (MetaMask, Rabby, etc.).
|
|
173
|
+
|
|
174
|
+
--broadcast
|
|
175
|
+
Sign and broadcast the transaction on-chain. Without this flag the
|
|
176
|
+
command performs a dry-run.
|
|
177
|
+
|
|
178
|
+
--out-dir <path>
|
|
179
|
+
Directory to write the result as a JSON file.
|
|
180
|
+
|
|
181
|
+
Behavior:
|
|
182
|
+
Reads existing registrations from app/erc-8004.ts. If there is one
|
|
183
|
+
registration, confirms it. If multiple, prompts you to select which
|
|
184
|
+
one to update. The chain and registry address are derived from the
|
|
185
|
+
selected registration's agentRegistry field.
|
|
186
|
+
|
|
187
|
+
Environment Variables:
|
|
188
|
+
PRIVATE_KEY Private key (hex, with or without 0x prefix) used for signing.
|
|
189
|
+
|
|
190
|
+
Examples:
|
|
191
|
+
# Dry-run (default)
|
|
192
|
+
$ aixyz erc-8004 update --url "https://new-domain.example.com"
|
|
193
|
+
|
|
194
|
+
# Sign and broadcast
|
|
195
|
+
$ aixyz erc-8004 update --url "https://new-domain.example.com" --keystore ~/.foundry/keystores/default --broadcast
|
|
196
|
+
$ aixyz erc-8004 update --url "https://new-domain.example.com" --browser --broadcast`,
|
|
197
|
+
)
|
|
198
|
+
.action(handleAction(update));
|
|
199
|
+
|
|
63
200
|
program.parse();
|
|
@@ -37,7 +37,7 @@ export function AixyzConfigPlugin(): BunPlugin {
|
|
|
37
37
|
return {
|
|
38
38
|
name: "aixyz-config",
|
|
39
39
|
setup(build) {
|
|
40
|
-
build.onLoad({ filter: /aixyz\/
|
|
40
|
+
build.onLoad({ filter: /aixyz[-/]config\/index\.ts$/ }, () => ({
|
|
41
41
|
contents: `
|
|
42
42
|
const config = ${JSON.stringify(materialized)};
|
|
43
43
|
export function getAixyzConfig() {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BunPlugin } from "bun";
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "fs";
|
|
3
3
|
import { resolve, relative, basename, join } from "path";
|
|
4
|
+
import { getAixyzConfig } from "@aixyz/config";
|
|
4
5
|
|
|
5
6
|
export function AixyzServerPlugin(entrypoint: string, mode: "vercel" | "standalone"): BunPlugin {
|
|
6
7
|
return {
|
|
@@ -51,6 +52,17 @@ export function getEntrypointMayGenerate(cwd: string, mode: "dev" | "build"): st
|
|
|
51
52
|
return entrypoint;
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
class AixyzGlob {
|
|
56
|
+
constructor(readonly config = getAixyzConfig()) {}
|
|
57
|
+
|
|
58
|
+
includes(file: string): boolean {
|
|
59
|
+
const included = this.config.build.includes.some((pattern) => new Bun.Glob(pattern).match(file));
|
|
60
|
+
if (!included) return false;
|
|
61
|
+
const excluded = this.config.build.excludes.some((pattern) => new Bun.Glob(pattern).match(file));
|
|
62
|
+
return !excluded;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
54
66
|
/**
|
|
55
67
|
* Generate server.ts content by scanning the app directory for agent.ts and tools/.
|
|
56
68
|
*
|
|
@@ -58,6 +70,7 @@ export function getEntrypointMayGenerate(cwd: string, mode: "dev" | "build"): st
|
|
|
58
70
|
* @param entrypointDir - Directory where the generated file will live (for computing relative imports).
|
|
59
71
|
*/
|
|
60
72
|
function generateServer(appDir: string, entrypointDir: string): string {
|
|
73
|
+
const glob = new AixyzGlob();
|
|
61
74
|
const rel = relative(entrypointDir, appDir);
|
|
62
75
|
const importPrefix = rel === "" ? "." : rel.startsWith(".") ? rel : `./${rel}`;
|
|
63
76
|
|
|
@@ -73,22 +86,20 @@ function generateServer(appDir: string, entrypointDir: string): string {
|
|
|
73
86
|
imports.push('import { facilitator } from "aixyz/accepts";');
|
|
74
87
|
}
|
|
75
88
|
|
|
76
|
-
const hasAgent = existsSync(resolve(appDir, "agent.ts"));
|
|
89
|
+
const hasAgent = existsSync(resolve(appDir, "agent.ts")) && glob.includes("agent.ts");
|
|
77
90
|
if (hasAgent) {
|
|
78
91
|
imports.push('import { useA2A } from "aixyz/server/adapters/a2a";');
|
|
79
92
|
imports.push(`import * as agent from "${importPrefix}/agent";`);
|
|
80
93
|
}
|
|
81
94
|
|
|
82
95
|
const toolsDir = resolve(appDir, "tools");
|
|
83
|
-
const tools: { name: string }[] = [];
|
|
96
|
+
const tools: { name: string; identifier: string }[] = [];
|
|
84
97
|
if (existsSync(toolsDir)) {
|
|
85
98
|
for (const file of readdirSync(toolsDir)) {
|
|
86
|
-
if (
|
|
99
|
+
if (glob.includes(`tools/${file}`)) {
|
|
87
100
|
const name = basename(file, ".ts");
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
tools.push({ name });
|
|
91
|
-
}
|
|
101
|
+
const identifier = toIdentifier(name);
|
|
102
|
+
tools.push({ name, identifier });
|
|
92
103
|
}
|
|
93
104
|
}
|
|
94
105
|
}
|
|
@@ -96,7 +107,7 @@ function generateServer(appDir: string, entrypointDir: string): string {
|
|
|
96
107
|
if (tools.length > 0) {
|
|
97
108
|
imports.push('import { AixyzMCP } from "aixyz/server/adapters/mcp";');
|
|
98
109
|
for (const tool of tools) {
|
|
99
|
-
imports.push(`import * as ${tool.
|
|
110
|
+
imports.push(`import * as ${tool.identifier} from "${importPrefix}/tools/${tool.name}";`);
|
|
100
111
|
}
|
|
101
112
|
}
|
|
102
113
|
|
|
@@ -111,12 +122,35 @@ function generateServer(appDir: string, entrypointDir: string): string {
|
|
|
111
122
|
if (tools.length > 0) {
|
|
112
123
|
body.push("const mcp = new AixyzMCP(server);");
|
|
113
124
|
for (const tool of tools) {
|
|
114
|
-
body.push(`await mcp.register("${tool.name}", ${tool.
|
|
125
|
+
body.push(`await mcp.register("${tool.name}", ${tool.identifier});`);
|
|
115
126
|
}
|
|
116
127
|
body.push("await mcp.connect();");
|
|
117
128
|
}
|
|
118
129
|
|
|
130
|
+
// If app/erc-8004.ts exists, auto-register ERC-8004 endpoint
|
|
131
|
+
const hasErc8004 = existsSync(resolve(appDir, "erc-8004.ts"));
|
|
132
|
+
if (hasErc8004) {
|
|
133
|
+
imports.push('import { useERC8004 } from "aixyz/server/adapters/erc-8004";');
|
|
134
|
+
imports.push(`import * as erc8004 from "${importPrefix}/erc-8004";`);
|
|
135
|
+
body.push(
|
|
136
|
+
`useERC8004(server, { default: erc8004.default, options: { mcp: ${tools.length > 0}, a2a: ${hasAgent} } });`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
119
140
|
body.push("export default server;");
|
|
120
141
|
|
|
121
142
|
return [...imports, "", ...body].join("\n");
|
|
122
143
|
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Convert a kebab-case filename into a valid JS identifier.
|
|
147
|
+
*
|
|
148
|
+
* Examples:
|
|
149
|
+
* "lookup" → "lookup"
|
|
150
|
+
* "get-aggregator-v3-address" → "getAggregatorV3Address"
|
|
151
|
+
* "3d-model" → "_3dModel"
|
|
152
|
+
*/
|
|
153
|
+
function toIdentifier(name: string): string {
|
|
154
|
+
const camel = name.replace(/-(.)/g, (_, c: string) => c.toUpperCase()).replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
155
|
+
return /^\d/.test(camel) ? `_${camel}` : camel;
|
|
156
|
+
}
|
package/build/index.ts
CHANGED
|
@@ -77,7 +77,7 @@ async function buildBun(entrypoint: string): Promise<void> {
|
|
|
77
77
|
console.log("Build complete! Output:");
|
|
78
78
|
console.log(" .aixyz/output/server.js");
|
|
79
79
|
console.log(" .aixyz/output/package.json");
|
|
80
|
-
if (existsSync(publicDir) ||
|
|
80
|
+
if (existsSync(publicDir) || iconFile) {
|
|
81
81
|
console.log(" .aixyz/output/public/ and assets");
|
|
82
82
|
}
|
|
83
83
|
console.log("");
|
package/dev/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ export async function dev(options: { port?: string }): Promise<void> {
|
|
|
15
15
|
const baseUrl = `http://localhost:${port}`;
|
|
16
16
|
|
|
17
17
|
console.log("");
|
|
18
|
-
console.log(`⟡
|
|
18
|
+
console.log(`⟡ aixyz.sh v${pkg.version}`);
|
|
19
19
|
console.log("");
|
|
20
20
|
console.log(`- A2A: ${baseUrl}/.well-known/agent-card.json`);
|
|
21
21
|
console.log(`- MCP: ${baseUrl}/mcp`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aixyz/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Payment-native SDK for AI Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"agent",
|
|
9
9
|
"aixyz"
|
|
10
10
|
],
|
|
11
|
-
"homepage": "https://
|
|
11
|
+
"homepage": "https://aixyz.sh",
|
|
12
12
|
"bugs": "https://github.com/AgentlyHQ/aixyz/issues",
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
@@ -23,15 +23,20 @@
|
|
|
23
23
|
"files": [
|
|
24
24
|
"build",
|
|
25
25
|
"dev",
|
|
26
|
+
"register",
|
|
26
27
|
"bin.ts"
|
|
27
28
|
],
|
|
28
29
|
"dependencies": {
|
|
29
|
-
"@aixyz/config": "0.
|
|
30
|
+
"@aixyz/config": "0.9.0",
|
|
31
|
+
"@aixyz/erc-8004": "0.9.0",
|
|
32
|
+
"@inquirer/prompts": "^8.3.0",
|
|
30
33
|
"@next/env": "^16.1.6",
|
|
31
|
-
"boxen": "^8.0.
|
|
32
|
-
"chalk": "^5.
|
|
34
|
+
"boxen": "^8.0.1",
|
|
35
|
+
"chalk": "^5.6.2",
|
|
33
36
|
"commander": "^14.0.3",
|
|
34
|
-
"
|
|
37
|
+
"ethers": "^6.16.0",
|
|
38
|
+
"sharp": "^0.34.5",
|
|
39
|
+
"viem": "^2.46.3"
|
|
35
40
|
},
|
|
36
41
|
"engines": {
|
|
37
42
|
"bun": ">=1.3.0"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { CHAIN_ID, getIdentityRegistryAddress } from "@aixyz/erc-8004";
|
|
3
|
+
|
|
4
|
+
describe("register command chain configuration", () => {
|
|
5
|
+
test("sepolia chain ID is correct", () => {
|
|
6
|
+
expect(CHAIN_ID.SEPOLIA).toStrictEqual(11155111);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("base-sepolia chain ID is correct", () => {
|
|
10
|
+
expect(CHAIN_ID.BASE_SEPOLIA).toStrictEqual(84532);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("identity registry address is returned for sepolia", () => {
|
|
14
|
+
const address = getIdentityRegistryAddress(CHAIN_ID.SEPOLIA);
|
|
15
|
+
expect(address).toMatch(/^0x[a-fA-F0-9]{40}$/);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("identity registry address is returned for base-sepolia", () => {
|
|
19
|
+
const address = getIdentityRegistryAddress(CHAIN_ID.BASE_SEPOLIA);
|
|
20
|
+
expect(address).toMatch(/^0x[a-fA-F0-9]{40}$/);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("throws for unsupported chain ID", () => {
|
|
24
|
+
expect(() => getIdentityRegistryAddress(999999)).toThrow("Unsupported chain ID");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("register command validation", () => {
|
|
29
|
+
test("supported chains list includes sepolia", () => {
|
|
30
|
+
const CHAINS: Record<string, { chainId: number }> = {
|
|
31
|
+
sepolia: { chainId: CHAIN_ID.SEPOLIA },
|
|
32
|
+
"base-sepolia": { chainId: CHAIN_ID.BASE_SEPOLIA },
|
|
33
|
+
};
|
|
34
|
+
expect(CHAINS["sepolia"]).toBeDefined();
|
|
35
|
+
expect(CHAINS["sepolia"].chainId).toStrictEqual(CHAIN_ID.SEPOLIA);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("supported chains list includes base-sepolia", () => {
|
|
39
|
+
const CHAINS: Record<string, { chainId: number }> = {
|
|
40
|
+
sepolia: { chainId: CHAIN_ID.SEPOLIA },
|
|
41
|
+
"base-sepolia": { chainId: CHAIN_ID.BASE_SEPOLIA },
|
|
42
|
+
localhost: { chainId: 31337 },
|
|
43
|
+
};
|
|
44
|
+
expect(CHAINS["base-sepolia"]).toBeDefined();
|
|
45
|
+
expect(CHAINS["base-sepolia"].chainId).toStrictEqual(CHAIN_ID.BASE_SEPOLIA);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("supported chains list includes localhost with chainId 31337", () => {
|
|
49
|
+
const CHAINS: Record<string, { chainId: number }> = {
|
|
50
|
+
sepolia: { chainId: CHAIN_ID.SEPOLIA },
|
|
51
|
+
"base-sepolia": { chainId: CHAIN_ID.BASE_SEPOLIA },
|
|
52
|
+
localhost: { chainId: 31337 },
|
|
53
|
+
};
|
|
54
|
+
expect(CHAINS["localhost"]).toBeDefined();
|
|
55
|
+
expect(CHAINS["localhost"].chainId).toStrictEqual(31337);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("unsupported chain is not in list", () => {
|
|
59
|
+
const CHAINS: Record<string, { chainId: number }> = {
|
|
60
|
+
sepolia: { chainId: CHAIN_ID.SEPOLIA },
|
|
61
|
+
"base-sepolia": { chainId: CHAIN_ID.BASE_SEPOLIA },
|
|
62
|
+
localhost: { chainId: 31337 },
|
|
63
|
+
};
|
|
64
|
+
expect(CHAINS["mainnet"]).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { encodeFunctionData, formatEther, parseEventLogs, type Chain, type Log } from "viem";
|
|
2
|
+
import { IdentityRegistryAbi } from "@aixyz/erc-8004";
|
|
3
|
+
import { selectWalletMethod, type WalletOptions } from "./wallet";
|
|
4
|
+
import { signTransaction } from "./wallet/sign";
|
|
5
|
+
import {
|
|
6
|
+
resolveChainConfig,
|
|
7
|
+
selectChain,
|
|
8
|
+
resolveRegistryAddress,
|
|
9
|
+
validateBrowserRpcConflict,
|
|
10
|
+
getExplorerUrl,
|
|
11
|
+
} from "./utils/chain";
|
|
12
|
+
import { writeResultJson } from "./utils/result";
|
|
13
|
+
import { label, truncateUri, broadcastAndConfirm, logSignResult } from "./utils/transaction";
|
|
14
|
+
import { promptAgentUrl, promptSupportedTrust, deriveAgentUri } from "./utils/prompt";
|
|
15
|
+
import { hasErc8004File, createErc8004File, writeRegistrationEntry } from "./utils/erc8004-file";
|
|
16
|
+
import { confirm } from "@inquirer/prompts";
|
|
17
|
+
import chalk from "chalk";
|
|
18
|
+
import boxen from "boxen";
|
|
19
|
+
import type { BaseOptions } from "./index";
|
|
20
|
+
|
|
21
|
+
export interface RegisterOptions extends BaseOptions {
|
|
22
|
+
url?: string;
|
|
23
|
+
chain?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function register(options: RegisterOptions): Promise<void> {
|
|
27
|
+
// Step 1: Ensure app/erc-8004.ts exists
|
|
28
|
+
if (!hasErc8004File()) {
|
|
29
|
+
console.log(chalk.yellow("No app/erc-8004.ts found. Let's create one."));
|
|
30
|
+
console.log("");
|
|
31
|
+
const supportedTrust = await promptSupportedTrust();
|
|
32
|
+
createErc8004File(supportedTrust);
|
|
33
|
+
console.log(chalk.green("Created app/erc-8004.ts"));
|
|
34
|
+
console.log("");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Step 2: Get agent URL and derive URI
|
|
38
|
+
const agentUrl = options.url ?? (await promptAgentUrl());
|
|
39
|
+
const resolvedUri = deriveAgentUri(agentUrl);
|
|
40
|
+
|
|
41
|
+
const yes = await confirm({
|
|
42
|
+
message: `Will register URI as: ${chalk.cyan(resolvedUri)} — confirm?`,
|
|
43
|
+
default: true,
|
|
44
|
+
});
|
|
45
|
+
if (!yes) {
|
|
46
|
+
throw new Error("Aborted.");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Step 3: Select chain
|
|
50
|
+
const chainName = options.chain ?? (await selectChain());
|
|
51
|
+
const chainConfig = resolveChainConfig(chainName);
|
|
52
|
+
const registryAddress = resolveRegistryAddress(chainName, chainConfig.chainId, options.registry);
|
|
53
|
+
|
|
54
|
+
// Step 4: Encode transaction
|
|
55
|
+
const data = encodeFunctionData({
|
|
56
|
+
abi: IdentityRegistryAbi,
|
|
57
|
+
functionName: "register",
|
|
58
|
+
args: [resolvedUri],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const printTxDetails = (header: string) => {
|
|
62
|
+
console.log("");
|
|
63
|
+
console.log(chalk.dim(header));
|
|
64
|
+
console.log(` ${label("To")}${registryAddress}`);
|
|
65
|
+
console.log(` ${label("Data")}${data.slice(0, 10)}${chalk.dim("\u2026" + (data.length - 2) / 2 + " bytes")}`);
|
|
66
|
+
console.log(` ${label("Chain")}${chainName}`);
|
|
67
|
+
console.log(` ${label("Function")}register(string memory agentURI)`);
|
|
68
|
+
console.log(` ${label("URI")}${truncateUri(resolvedUri)}`);
|
|
69
|
+
console.log("");
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
validateBrowserRpcConflict(options.browser, options.rpcUrl);
|
|
73
|
+
|
|
74
|
+
if (!options.broadcast) {
|
|
75
|
+
if (options.browser || options.keystore || process.env.PRIVATE_KEY) {
|
|
76
|
+
console.warn("Note: --browser/--keystore/PRIVATE_KEY ignored in dry-run mode. Pass --broadcast to use a wallet.");
|
|
77
|
+
}
|
|
78
|
+
printTxDetails("Transaction details (dry-run)");
|
|
79
|
+
console.log("Dry-run complete. To sign and broadcast, re-run with --broadcast.");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const walletMethod = await selectWalletMethod(options);
|
|
84
|
+
validateBrowserRpcConflict(walletMethod.type === "browser" || undefined, options.rpcUrl);
|
|
85
|
+
|
|
86
|
+
printTxDetails("Signing transaction...");
|
|
87
|
+
|
|
88
|
+
const result = await signTransaction({
|
|
89
|
+
walletMethod,
|
|
90
|
+
tx: { to: registryAddress, data },
|
|
91
|
+
chain: chainConfig.chain,
|
|
92
|
+
rpcUrl: options.rpcUrl,
|
|
93
|
+
options: {
|
|
94
|
+
browser: { chainId: chainConfig.chainId, chainName, uri: resolvedUri, mode: "register" },
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
logSignResult(walletMethod.type, result);
|
|
98
|
+
|
|
99
|
+
const { hash, receipt, timestamp } = await broadcastAndConfirm({
|
|
100
|
+
result,
|
|
101
|
+
chain: chainConfig.chain,
|
|
102
|
+
rpcUrl: options.rpcUrl,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const resultData = printResult(receipt, timestamp, chainConfig.chain, chainConfig.chainId, hash);
|
|
106
|
+
|
|
107
|
+
// Step 5: Write registration entry back to app/erc-8004.ts
|
|
108
|
+
if (resultData.agentId !== undefined) {
|
|
109
|
+
const agentRegistry = `eip155:${chainConfig.chainId}:${registryAddress}`;
|
|
110
|
+
writeRegistrationEntry({ agentId: Number(resultData.agentId), agentRegistry });
|
|
111
|
+
console.log("");
|
|
112
|
+
console.log(chalk.green(`Updated app/erc-8004.ts with registration (agentId: ${resultData.agentId})`));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (options.outDir) {
|
|
116
|
+
writeResultJson(options.outDir, "registration", resultData);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface RegistrationResult {
|
|
121
|
+
agentId?: string;
|
|
122
|
+
owner?: string;
|
|
123
|
+
uri?: string;
|
|
124
|
+
chainId: number;
|
|
125
|
+
block: string;
|
|
126
|
+
timestamp: string;
|
|
127
|
+
gasPaid: string;
|
|
128
|
+
nativeCurrency: string;
|
|
129
|
+
txHash: string;
|
|
130
|
+
explorer?: string;
|
|
131
|
+
metadata?: Record<string, string>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function printResult(
|
|
135
|
+
receipt: { blockNumber: bigint; gasUsed: bigint; effectiveGasPrice: bigint; logs: Log[] },
|
|
136
|
+
timestamp: bigint,
|
|
137
|
+
chain: Chain,
|
|
138
|
+
chainId: number,
|
|
139
|
+
hash: `0x${string}`,
|
|
140
|
+
): RegistrationResult {
|
|
141
|
+
const events = parseEventLogs({ abi: IdentityRegistryAbi, logs: receipt.logs });
|
|
142
|
+
const registered = events.find((e) => e.eventName === "Registered");
|
|
143
|
+
const metadataEvents = events.filter((e) => e.eventName === "MetadataSet");
|
|
144
|
+
|
|
145
|
+
const lines: string[] = [];
|
|
146
|
+
const result: RegistrationResult = {
|
|
147
|
+
chainId,
|
|
148
|
+
block: receipt.blockNumber.toString(),
|
|
149
|
+
timestamp: new Date(Number(timestamp) * 1000).toUTCString(),
|
|
150
|
+
gasPaid: formatEther(receipt.gasUsed * receipt.effectiveGasPrice),
|
|
151
|
+
nativeCurrency: chain.nativeCurrency?.symbol ?? "ETH",
|
|
152
|
+
txHash: hash,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
if (registered) {
|
|
156
|
+
const { agentId, agentURI, owner } = registered.args as { agentId: bigint; agentURI: string; owner: string };
|
|
157
|
+
result.agentId = agentId.toString();
|
|
158
|
+
result.owner = owner;
|
|
159
|
+
if (agentURI) result.uri = agentURI;
|
|
160
|
+
|
|
161
|
+
lines.push(`${label("Agent ID")}${chalk.bold(result.agentId)}`);
|
|
162
|
+
lines.push(`${label("Owner")}${owner}`);
|
|
163
|
+
if (agentURI) {
|
|
164
|
+
lines.push(`${label("URI")}${truncateUri(agentURI)}`);
|
|
165
|
+
}
|
|
166
|
+
lines.push(`${label("Block")}${receipt.blockNumber}`);
|
|
167
|
+
} else {
|
|
168
|
+
lines.push(`${label("Block")}${receipt.blockNumber}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
lines.push(`${label("Timestamp")}${result.timestamp}`);
|
|
172
|
+
lines.push(`${label("Gas Paid")}${result.gasPaid} ${result.nativeCurrency}`);
|
|
173
|
+
lines.push(`${label("Tx Hash")}${hash}`);
|
|
174
|
+
|
|
175
|
+
const explorerUrl = getExplorerUrl(chain, hash);
|
|
176
|
+
if (explorerUrl) {
|
|
177
|
+
result.explorer = explorerUrl;
|
|
178
|
+
lines.push(`${label("Explorer")}${chalk.cyan(explorerUrl)}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (metadataEvents.length > 0) {
|
|
182
|
+
result.metadata = {};
|
|
183
|
+
lines.push("");
|
|
184
|
+
lines.push(chalk.dim("Metadata"));
|
|
185
|
+
for (const event of metadataEvents) {
|
|
186
|
+
const { metadataKey, metadataValue } = event.args as { metadataKey: string; metadataValue: string };
|
|
187
|
+
result.metadata[metadataKey] = metadataValue;
|
|
188
|
+
lines.push(`${label(metadataKey)}${metadataValue}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
console.log("");
|
|
193
|
+
console.log(
|
|
194
|
+
boxen(lines.join("\n"), {
|
|
195
|
+
padding: { left: 1, right: 1, top: 0, bottom: 0 },
|
|
196
|
+
borderStyle: "round",
|
|
197
|
+
borderColor: "green",
|
|
198
|
+
title: "Agent registered successfully",
|
|
199
|
+
titleAlignment: "left",
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
@@ -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
|
+
});
|