@aixyz/cli 0.10.0 → 0.12.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/build/AixyzServerPlugin.ts +30 -5
- package/build/index.ts +78 -3
- package/dev/index.ts +3 -1
- package/package.json +3 -3
- package/register/index.ts +43 -17
- package/register/register.ts +27 -13
- package/register/update.test.ts +119 -1
- package/register/update.ts +24 -10
- package/register/utils/chain.ts +175 -15
- package/register/utils/prompt.ts +88 -32
- package/register/wallet/index.ts +31 -28
|
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readdirSync, writeFileSync } from "fs";
|
|
|
3
3
|
import { resolve, relative, basename, join } from "path";
|
|
4
4
|
import { getAixyzConfig } from "@aixyz/config";
|
|
5
5
|
|
|
6
|
-
export function AixyzServerPlugin(entrypoint: string, mode: "vercel" | "standalone"): BunPlugin {
|
|
6
|
+
export function AixyzServerPlugin(entrypoint: string, mode: "vercel" | "standalone" | "executable"): BunPlugin {
|
|
7
7
|
return {
|
|
8
8
|
name: "aixyz-entrypoint",
|
|
9
9
|
setup(build) {
|
|
@@ -17,7 +17,7 @@ export function AixyzServerPlugin(entrypoint: string, mode: "vercel" | "standalo
|
|
|
17
17
|
const transformed = source.replace(/export\s+default\s+(\w+)\s*;/, "export default $1.express;");
|
|
18
18
|
return { contents: transformed, loader: "ts" };
|
|
19
19
|
} else {
|
|
20
|
-
// For standalone, keep the server export but add startup code
|
|
20
|
+
// For standalone and executable, keep the server export but add startup code
|
|
21
21
|
// TODO(@fuxingloh): use Bun.serve later.
|
|
22
22
|
const transformed = source.replace(
|
|
23
23
|
/export\s+default\s+(\w+)\s*;/,
|
|
@@ -64,9 +64,9 @@ class AixyzGlob {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
|
-
* Generate server.ts content by scanning the app directory for agent.ts and tools/.
|
|
67
|
+
* Generate server.ts content by scanning the app directory for agent.ts, agents/, and tools/.
|
|
68
68
|
*
|
|
69
|
-
* @param appDir - The app directory containing agent.ts and tools/
|
|
69
|
+
* @param appDir - The app directory containing agent.ts, agents/, and tools/
|
|
70
70
|
* @param entrypointDir - Directory where the generated file will live (for computing relative imports).
|
|
71
71
|
*/
|
|
72
72
|
function generateServer(appDir: string, entrypointDir: string): string {
|
|
@@ -92,6 +92,25 @@ function generateServer(appDir: string, entrypointDir: string): string {
|
|
|
92
92
|
imports.push(`import * as agent from "${importPrefix}/agent";`);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
const agentsDir = resolve(appDir, "agents");
|
|
96
|
+
const subAgents: { name: string; identifier: string }[] = [];
|
|
97
|
+
if (existsSync(agentsDir)) {
|
|
98
|
+
for (const file of readdirSync(agentsDir)) {
|
|
99
|
+
if (glob.includes(`agents/${file}`)) {
|
|
100
|
+
const name = basename(file, ".ts");
|
|
101
|
+
const identifier = toIdentifier(name);
|
|
102
|
+
subAgents.push({ name, identifier });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!hasAgent && subAgents.length > 0) {
|
|
108
|
+
imports.push('import { useA2A } from "aixyz/server/adapters/a2a";');
|
|
109
|
+
}
|
|
110
|
+
for (const subAgent of subAgents) {
|
|
111
|
+
imports.push(`import * as ${subAgent.identifier} from "${importPrefix}/agents/${subAgent.name}";`);
|
|
112
|
+
}
|
|
113
|
+
|
|
95
114
|
const toolsDir = resolve(appDir, "tools");
|
|
96
115
|
const tools: { name: string; identifier: string }[] = [];
|
|
97
116
|
if (existsSync(toolsDir)) {
|
|
@@ -119,6 +138,9 @@ function generateServer(appDir: string, entrypointDir: string): string {
|
|
|
119
138
|
body.push("useA2A(server, agent);");
|
|
120
139
|
}
|
|
121
140
|
|
|
141
|
+
for (const subAgent of subAgents) {
|
|
142
|
+
body.push(`useA2A(server, ${subAgent.identifier}, "${subAgent.name}");`);
|
|
143
|
+
}
|
|
122
144
|
if (tools.length > 0) {
|
|
123
145
|
body.push("const mcp = new AixyzMCP(server);");
|
|
124
146
|
for (const tool of tools) {
|
|
@@ -132,8 +154,11 @@ function generateServer(appDir: string, entrypointDir: string): string {
|
|
|
132
154
|
if (hasErc8004) {
|
|
133
155
|
imports.push('import { useERC8004 } from "aixyz/server/adapters/erc-8004";');
|
|
134
156
|
imports.push(`import * as erc8004 from "${importPrefix}/erc-8004";`);
|
|
157
|
+
const a2aPaths: string[] = [];
|
|
158
|
+
if (hasAgent) a2aPaths.push("/.well-known/agent-card.json");
|
|
159
|
+
for (const subAgent of subAgents) a2aPaths.push(`/${subAgent.name}/.well-known/agent-card.json`);
|
|
135
160
|
body.push(
|
|
136
|
-
`useERC8004(server, { default: erc8004.default, options: { mcp: ${tools.length > 0}, a2a: ${
|
|
161
|
+
`useERC8004(server, { default: erc8004.default, options: { mcp: ${tools.length > 0}, a2a: ${JSON.stringify(a2aPaths)} } });`,
|
|
137
162
|
);
|
|
138
163
|
}
|
|
139
164
|
|
package/build/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ interface BuildOptions {
|
|
|
14
14
|
|
|
15
15
|
export const buildCommand = new Command("build")
|
|
16
16
|
.description("Build the aixyz agent")
|
|
17
|
-
.option("--output <type>", "Output format: 'standalone' or '
|
|
17
|
+
.option("--output <type>", "Output format: 'standalone', 'vercel', or 'executable'")
|
|
18
18
|
.addHelpText(
|
|
19
19
|
"after",
|
|
20
20
|
`
|
|
@@ -28,6 +28,10 @@ Details:
|
|
|
28
28
|
Generates Vercel Build Output API v3 structure at .vercel/output/
|
|
29
29
|
(Automatically detected when deploying to Vercel)
|
|
30
30
|
|
|
31
|
+
With --output executable:
|
|
32
|
+
Compiles into a self-contained binary at ./.aixyz/output/server
|
|
33
|
+
(No Bun runtime required to run the output)
|
|
34
|
+
|
|
31
35
|
The build process:
|
|
32
36
|
1. Loads aixyz.config.ts from the current directory
|
|
33
37
|
2. Detects entrypoint (app/server.ts or auto-generates from app/agent.ts + app/tools/)
|
|
@@ -42,6 +46,7 @@ Examples:
|
|
|
42
46
|
$ aixyz build # Build standalone (default)
|
|
43
47
|
$ aixyz build --output standalone # Build standalone explicitly
|
|
44
48
|
$ aixyz build --output vercel # Build for Vercel deployment
|
|
49
|
+
$ aixyz build --output executable # Build self-contained binary
|
|
45
50
|
$ VERCEL=1 aixyz build # Auto-detected Vercel build`,
|
|
46
51
|
)
|
|
47
52
|
.action(action);
|
|
@@ -49,6 +54,8 @@ Examples:
|
|
|
49
54
|
async function action(options: BuildOptions = {}): Promise<void> {
|
|
50
55
|
const cwd = process.cwd();
|
|
51
56
|
loadEnvConfig(cwd, false);
|
|
57
|
+
process.env.NODE_ENV = "production";
|
|
58
|
+
process.env.AIXYZ_ENV = "production";
|
|
52
59
|
const entrypoint = getEntrypointMayGenerate(cwd, "build");
|
|
53
60
|
|
|
54
61
|
// Determine output target: explicit CLI flag takes precedence, then config file, then auto-detect VERCEL env
|
|
@@ -57,7 +64,10 @@ async function action(options: BuildOptions = {}): Promise<void> {
|
|
|
57
64
|
|
|
58
65
|
if (target === "vercel") {
|
|
59
66
|
console.log(chalk.cyan("▶") + " Building for " + chalk.bold("Vercel") + "...");
|
|
60
|
-
await buildVercel(entrypoint);
|
|
67
|
+
await buildVercel(entrypoint, config);
|
|
68
|
+
} else if (target === "executable") {
|
|
69
|
+
console.log(chalk.cyan("▶") + " Building " + chalk.bold("Executable") + "...");
|
|
70
|
+
await buildExecutable(entrypoint);
|
|
61
71
|
} else {
|
|
62
72
|
console.log(chalk.cyan("▶") + " Building for " + chalk.bold("Standalone") + "...");
|
|
63
73
|
await buildBun(entrypoint);
|
|
@@ -79,6 +89,10 @@ async function buildBun(entrypoint: string): Promise<void> {
|
|
|
79
89
|
target: "bun",
|
|
80
90
|
format: "esm",
|
|
81
91
|
sourcemap: "linked",
|
|
92
|
+
define: {
|
|
93
|
+
"process.env.NODE_ENV": JSON.stringify("production"),
|
|
94
|
+
"process.env.AIXYZ_ENV": JSON.stringify("production"),
|
|
95
|
+
},
|
|
82
96
|
plugins: [AixyzConfigPlugin(), AixyzServerPlugin(entrypoint, "standalone")],
|
|
83
97
|
});
|
|
84
98
|
|
|
@@ -119,7 +133,63 @@ async function buildBun(entrypoint: string): Promise<void> {
|
|
|
119
133
|
console.log("To run: bun .aixyz/output/server.js");
|
|
120
134
|
}
|
|
121
135
|
|
|
122
|
-
async function
|
|
136
|
+
async function buildExecutable(entrypoint: string): Promise<void> {
|
|
137
|
+
const cwd = process.cwd();
|
|
138
|
+
|
|
139
|
+
const outputDir = resolve(cwd, ".aixyz/output");
|
|
140
|
+
rmSync(outputDir, { recursive: true, force: true });
|
|
141
|
+
mkdirSync(outputDir, { recursive: true });
|
|
142
|
+
|
|
143
|
+
const outfile = resolve(outputDir, "server");
|
|
144
|
+
|
|
145
|
+
// Build as a self-contained compiled binary using Bun's compile feature
|
|
146
|
+
const result = await Bun.build({
|
|
147
|
+
entrypoints: [entrypoint],
|
|
148
|
+
outdir: outputDir,
|
|
149
|
+
target: "bun",
|
|
150
|
+
sourcemap: "linked",
|
|
151
|
+
compile: { outfile },
|
|
152
|
+
define: {
|
|
153
|
+
"process.env.NODE_ENV": JSON.stringify("production"),
|
|
154
|
+
"process.env.AIXYZ_ENV": JSON.stringify("production"),
|
|
155
|
+
},
|
|
156
|
+
plugins: [AixyzConfigPlugin(), AixyzServerPlugin(entrypoint, "executable")],
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (!result.success) {
|
|
160
|
+
console.error("Build failed:");
|
|
161
|
+
for (const log of result.logs) {
|
|
162
|
+
console.error(log);
|
|
163
|
+
}
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Copy static assets (public/ → .aixyz/output/public/)
|
|
168
|
+
const publicDir = resolve(cwd, "public");
|
|
169
|
+
if (existsSync(publicDir)) {
|
|
170
|
+
const destPublicDir = resolve(outputDir, "public");
|
|
171
|
+
cpSync(publicDir, destPublicDir, { recursive: true });
|
|
172
|
+
console.log("Copied public/ →", destPublicDir);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const iconFile = findIconFile(resolve(cwd, "app"));
|
|
176
|
+
if (iconFile) {
|
|
177
|
+
await copyAgentIcon(iconFile, resolve(outputDir, "icon.png"));
|
|
178
|
+
await generateFavicon(iconFile, resolve(outputDir, "public/favicon.ico"));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Log summary
|
|
182
|
+
console.log("");
|
|
183
|
+
console.log("Build complete! Output:");
|
|
184
|
+
console.log(" .aixyz/output/server");
|
|
185
|
+
if (existsSync(publicDir) || iconFile) {
|
|
186
|
+
console.log(" .aixyz/output/public/ and assets");
|
|
187
|
+
}
|
|
188
|
+
console.log("");
|
|
189
|
+
console.log("To run: ./.aixyz/output/server");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function buildVercel(entrypoint: string, config: ReturnType<typeof getAixyzConfig>): Promise<void> {
|
|
123
193
|
const cwd = process.cwd();
|
|
124
194
|
|
|
125
195
|
const outputDir = resolve(cwd, ".vercel/output");
|
|
@@ -136,6 +206,10 @@ async function buildVercel(entrypoint: string): Promise<void> {
|
|
|
136
206
|
target: "bun",
|
|
137
207
|
format: "esm",
|
|
138
208
|
sourcemap: "linked",
|
|
209
|
+
define: {
|
|
210
|
+
"process.env.NODE_ENV": JSON.stringify("production"),
|
|
211
|
+
"process.env.AIXYZ_ENV": JSON.stringify("production"),
|
|
212
|
+
},
|
|
139
213
|
plugins: [AixyzConfigPlugin(), AixyzServerPlugin(entrypoint, "vercel")],
|
|
140
214
|
});
|
|
141
215
|
|
|
@@ -155,6 +229,7 @@ async function buildVercel(entrypoint: string): Promise<void> {
|
|
|
155
229
|
handler: "server.js",
|
|
156
230
|
runtime: "bun1.x",
|
|
157
231
|
launcherType: "Bun",
|
|
232
|
+
maxDuration: config.vercel.maxDuration,
|
|
158
233
|
shouldAddHelpers: true,
|
|
159
234
|
shouldAddSourcemapSupport: true,
|
|
160
235
|
},
|
package/dev/index.ts
CHANGED
|
@@ -15,6 +15,8 @@ async function action(options: { port?: string }): Promise<void> {
|
|
|
15
15
|
|
|
16
16
|
// Load environment config
|
|
17
17
|
const { loadedEnvFiles } = loadEnvConfig(cwd, true);
|
|
18
|
+
process.env.NODE_ENV = "development";
|
|
19
|
+
process.env.AIXYZ_ENV = "development";
|
|
18
20
|
const envFileNames = loadedEnvFiles.map((f) => relative(cwd, f.path));
|
|
19
21
|
|
|
20
22
|
const port = options.port || process.env.PORT || "3000";
|
|
@@ -41,7 +43,7 @@ async function action(options: { port?: string }): Promise<void> {
|
|
|
41
43
|
cwd,
|
|
42
44
|
stdout: "inherit",
|
|
43
45
|
stderr: "inherit",
|
|
44
|
-
env: process.env,
|
|
46
|
+
env: { ...process.env, NODE_ENV: "development", AIXYZ_ENV: "development" },
|
|
45
47
|
});
|
|
46
48
|
child.exited.then((code) => {
|
|
47
49
|
if (!restarting && code !== 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aixyz/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Payment-native SDK for AI Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"bin.ts"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@aixyz/config": "0.
|
|
31
|
-
"@aixyz/erc-8004": "0.
|
|
30
|
+
"@aixyz/config": "0.12.0",
|
|
31
|
+
"@aixyz/erc-8004": "0.12.0",
|
|
32
32
|
"@inquirer/prompts": "^8.3.0",
|
|
33
33
|
"@next/env": "^16.1.6",
|
|
34
34
|
"boxen": "^8.0.1",
|
package/register/index.ts
CHANGED
|
@@ -4,7 +4,6 @@ import { update } from "./update";
|
|
|
4
4
|
import type { WalletOptions } from "./wallet";
|
|
5
5
|
|
|
6
6
|
export interface BaseOptions extends WalletOptions {
|
|
7
|
-
chain?: string;
|
|
8
7
|
rpcUrl?: string;
|
|
9
8
|
registry?: string;
|
|
10
9
|
outDir?: string;
|
|
@@ -16,13 +15,17 @@ erc8004Command
|
|
|
16
15
|
.command("register")
|
|
17
16
|
.description("Register a new agent to the ERC-8004 IdentityRegistry")
|
|
18
17
|
.option("--url <url>", "Agent deployment URL (e.g., https://my-agent.example.com)")
|
|
19
|
-
.option("--chain <
|
|
18
|
+
.option("--chain-id <chainId>", "Target chain by numeric chain ID", parseInt)
|
|
20
19
|
.option("--rpc-url <url>", "Custom RPC URL (uses default if not provided)")
|
|
21
|
-
.option("--registry <address>", "Contract address of the IdentityRegistry (required for localhost)")
|
|
20
|
+
.option("--registry <address>", "Contract address of the IdentityRegistry (required for localhost and custom chains)")
|
|
22
21
|
.option("--keystore <path>", "Path to Ethereum keystore (V3) JSON file for local signing")
|
|
23
22
|
.option("--browser", "Use browser extension wallet (any extension)")
|
|
24
23
|
.option("--broadcast", "Sign and broadcast the transaction (default: dry-run)")
|
|
25
24
|
.option("--out-dir <path>", "Write deployment result as JSON to the given directory")
|
|
25
|
+
.option(
|
|
26
|
+
"--supported-trust <values>",
|
|
27
|
+
'Comma-separated supported trust mechanisms (e.g., "reputation,tee-attestation"). If omitted and app/erc-8004.ts does not exist, you will be prompted interactively.',
|
|
28
|
+
)
|
|
26
29
|
.addHelpText(
|
|
27
30
|
"after",
|
|
28
31
|
`
|
|
@@ -32,22 +35,23 @@ Option Details:
|
|
|
32
35
|
The registration URI will be derived as <url>/_aixyz/erc-8004.json.
|
|
33
36
|
If omitted, you will be prompted to enter the URL interactively.
|
|
34
37
|
|
|
35
|
-
--chain <
|
|
36
|
-
Target chain
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
base-sepolia
|
|
40
|
-
localhost Local Foundry/Anvil node (chain ID 31337)
|
|
38
|
+
--chain-id <chainId>
|
|
39
|
+
Target chain by numeric chain ID. Supported chain IDs include:
|
|
40
|
+
1 (mainnet), 8453 (base), 42161 (arbitrum), 10 (optimism),
|
|
41
|
+
137 (polygon), 56 (bsc), 43114 (avalanche), 11155111 (sepolia),
|
|
42
|
+
84532 (base-sepolia), and more (see @aixyz/erc-8004).
|
|
41
43
|
If omitted, you will be prompted to select a chain interactively.
|
|
44
|
+
Use any custom EVM chain ID with --rpc-url for BYO chains.
|
|
42
45
|
|
|
43
46
|
--rpc-url <url>
|
|
44
47
|
Custom RPC endpoint URL. Overrides the default RPC for the selected
|
|
45
|
-
chain.
|
|
48
|
+
chain. Required when using a custom chain ID with no default deployment.
|
|
49
|
+
Cannot be used with --browser since the browser wallet manages
|
|
46
50
|
its own RPC connection.
|
|
47
51
|
|
|
48
52
|
--registry <address>
|
|
49
|
-
Contract address of the ERC-8004 IdentityRegistry.
|
|
50
|
-
localhost, where there is no default deployment.
|
|
53
|
+
Contract address of the ERC-8004 IdentityRegistry. Required for
|
|
54
|
+
localhost and custom chains, where there is no default deployment.
|
|
51
55
|
|
|
52
56
|
--keystore <path>
|
|
53
57
|
Path to an Ethereum keystore (V3) JSON file. You will be prompted for
|
|
@@ -64,6 +68,13 @@ Option Details:
|
|
|
64
68
|
--out-dir <path>
|
|
65
69
|
Directory to write the deployment result as a JSON file.
|
|
66
70
|
|
|
71
|
+
--supported-trust <values>
|
|
72
|
+
Comma-separated list of trust mechanisms to declare in app/erc-8004.ts
|
|
73
|
+
when it does not yet exist. Valid values: reputation, crypto-economic,
|
|
74
|
+
tee-attestation, social, governance.
|
|
75
|
+
Example: --supported-trust "reputation,tee-attestation"
|
|
76
|
+
If omitted, you will be prompted to select interactively.
|
|
77
|
+
|
|
67
78
|
Behavior:
|
|
68
79
|
If app/erc-8004.ts does not exist, you will be prompted to create it
|
|
69
80
|
(selecting supported trust mechanisms). After a successful on-chain
|
|
@@ -74,11 +85,17 @@ Environment Variables:
|
|
|
74
85
|
|
|
75
86
|
Examples:
|
|
76
87
|
# Dry-run (default)
|
|
77
|
-
$ aixyz erc-8004 register --url "https://my-agent.example.com" --chain
|
|
88
|
+
$ aixyz erc-8004 register --url "https://my-agent.example.com" --chain-id 11155111
|
|
78
89
|
|
|
79
|
-
# Sign and broadcast
|
|
80
|
-
$ aixyz erc-8004 register --url "https://my-agent.example.com" --chain
|
|
81
|
-
$ aixyz erc-8004 register --url "https://my-agent.example.com" --chain
|
|
90
|
+
# Sign and broadcast (known chain)
|
|
91
|
+
$ aixyz erc-8004 register --url "https://my-agent.example.com" --chain-id 84532 --keystore ~/.foundry/keystores/default --broadcast
|
|
92
|
+
$ aixyz erc-8004 register --url "https://my-agent.example.com" --chain-id 84532 --browser --broadcast
|
|
93
|
+
|
|
94
|
+
# Non-interactive (CI-friendly)
|
|
95
|
+
$ aixyz erc-8004 register --url "https://my-agent.example.com" --chain-id 84532 --supported-trust "reputation,tee-attestation" --keystore ~/.foundry/keystores/default --broadcast
|
|
96
|
+
|
|
97
|
+
# BYO: register on any custom EVM chain
|
|
98
|
+
$ aixyz erc-8004 register --url "https://my-agent.example.com" --chain-id 999999 --rpc-url https://my-rpc.example.com --registry 0xABCD... --broadcast`,
|
|
82
99
|
)
|
|
83
100
|
.action(register);
|
|
84
101
|
|
|
@@ -86,6 +103,7 @@ erc8004Command
|
|
|
86
103
|
.command("update")
|
|
87
104
|
.description("Update the metadata URI of a registered agent")
|
|
88
105
|
.option("--url <url>", "New agent deployment URL (e.g., https://my-agent.example.com)")
|
|
106
|
+
.option("--agent-id <id>", "Agent ID to update (selects which registration from app/erc-8004.ts)", parseInt)
|
|
89
107
|
.option("--rpc-url <url>", "Custom RPC URL (uses default if not provided)")
|
|
90
108
|
.option("--registry <address>", "Contract address of the IdentityRegistry (required for localhost)")
|
|
91
109
|
.option("--keystore <path>", "Path to Ethereum keystore (V3) JSON file for local signing")
|
|
@@ -101,6 +119,11 @@ Option Details:
|
|
|
101
119
|
The URI will be derived as <url>/_aixyz/erc-8004.json.
|
|
102
120
|
If omitted, you will be prompted to enter the URL interactively.
|
|
103
121
|
|
|
122
|
+
--agent-id <id>
|
|
123
|
+
Agent ID (numeric) to select which registration from app/erc-8004.ts
|
|
124
|
+
to update. Required in non-interactive (non-TTY) mode when multiple
|
|
125
|
+
registrations exist.
|
|
126
|
+
|
|
104
127
|
--rpc-url <url>
|
|
105
128
|
Custom RPC endpoint URL. Overrides the default RPC for the selected
|
|
106
129
|
chain. Cannot be used with --browser.
|
|
@@ -138,6 +161,9 @@ Examples:
|
|
|
138
161
|
|
|
139
162
|
# Sign and broadcast
|
|
140
163
|
$ aixyz erc-8004 update --url "https://new-domain.example.com" --keystore ~/.foundry/keystores/default --broadcast
|
|
141
|
-
$ aixyz erc-8004 update --url "https://new-domain.example.com" --browser --broadcast
|
|
164
|
+
$ aixyz erc-8004 update --url "https://new-domain.example.com" --browser --broadcast
|
|
165
|
+
|
|
166
|
+
# Non-interactive (CI-friendly)
|
|
167
|
+
$ aixyz erc-8004 update --url "https://new-domain.example.com" --agent-id 42 --keystore ~/.foundry/keystores/default --broadcast`,
|
|
142
168
|
)
|
|
143
169
|
.action(update);
|
package/register/register.ts
CHANGED
|
@@ -3,15 +3,23 @@ import { IdentityRegistryAbi } from "@aixyz/erc-8004";
|
|
|
3
3
|
import { selectWalletMethod } from "./wallet";
|
|
4
4
|
import { signTransaction } from "./wallet/sign";
|
|
5
5
|
import {
|
|
6
|
-
|
|
6
|
+
resolveChainConfigById,
|
|
7
7
|
selectChain,
|
|
8
8
|
resolveRegistryAddress,
|
|
9
9
|
validateBrowserRpcConflict,
|
|
10
10
|
getExplorerUrl,
|
|
11
|
+
CHAINS,
|
|
11
12
|
} from "./utils/chain";
|
|
12
13
|
import { writeResultJson } from "./utils/result";
|
|
13
14
|
import { label, truncateUri, broadcastAndConfirm, logSignResult } from "./utils/transaction";
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
promptAgentUrl,
|
|
17
|
+
promptSupportedTrust,
|
|
18
|
+
promptRegistryAddress,
|
|
19
|
+
deriveAgentUri,
|
|
20
|
+
isTTY,
|
|
21
|
+
parseSupportedTrust,
|
|
22
|
+
} from "./utils/prompt";
|
|
15
23
|
import { hasErc8004File, createErc8004File, writeRegistrationEntry } from "./utils/erc8004-file";
|
|
16
24
|
import { confirm } from "@inquirer/prompts";
|
|
17
25
|
import chalk from "chalk";
|
|
@@ -20,7 +28,8 @@ import type { BaseOptions } from "./index";
|
|
|
20
28
|
|
|
21
29
|
export interface RegisterOptions extends BaseOptions {
|
|
22
30
|
url?: string;
|
|
23
|
-
|
|
31
|
+
chainId?: number;
|
|
32
|
+
supportedTrust?: string;
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
export async function register(options: RegisterOptions): Promise<void> {
|
|
@@ -28,7 +37,9 @@ export async function register(options: RegisterOptions): Promise<void> {
|
|
|
28
37
|
if (!hasErc8004File()) {
|
|
29
38
|
console.log(chalk.yellow("No app/erc-8004.ts found. Let's create one."));
|
|
30
39
|
console.log("");
|
|
31
|
-
const supportedTrust =
|
|
40
|
+
const supportedTrust = options.supportedTrust
|
|
41
|
+
? parseSupportedTrust(options.supportedTrust)
|
|
42
|
+
: await promptSupportedTrust();
|
|
32
43
|
createErc8004File(supportedTrust);
|
|
33
44
|
console.log(chalk.green("Created app/erc-8004.ts"));
|
|
34
45
|
console.log("");
|
|
@@ -38,18 +49,21 @@ export async function register(options: RegisterOptions): Promise<void> {
|
|
|
38
49
|
const agentUrl = options.url ?? (await promptAgentUrl());
|
|
39
50
|
const resolvedUri = deriveAgentUri(agentUrl);
|
|
40
51
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
if (isTTY()) {
|
|
53
|
+
const yes = await confirm({
|
|
54
|
+
message: `Will register URI as: ${chalk.cyan(resolvedUri)} — confirm?`,
|
|
55
|
+
default: true,
|
|
56
|
+
});
|
|
57
|
+
if (!yes) {
|
|
58
|
+
throw new Error("Aborted.");
|
|
59
|
+
}
|
|
47
60
|
}
|
|
48
61
|
|
|
49
62
|
// Step 3: Select chain
|
|
50
|
-
const
|
|
51
|
-
const chainConfig =
|
|
52
|
-
const
|
|
63
|
+
const chainId = options.chainId ?? (await selectChain());
|
|
64
|
+
const chainConfig = resolveChainConfigById(chainId, options.rpcUrl);
|
|
65
|
+
const chainName = Object.entries(CHAINS).find(([, c]) => c.chainId === chainId)?.[0] ?? `chain-${chainId}`;
|
|
66
|
+
const registryAddress = resolveRegistryAddress(chainId, options.registry) ?? (await promptRegistryAddress());
|
|
53
67
|
|
|
54
68
|
// Step 4: Encode transaction
|
|
55
69
|
const data = encodeFunctionData({
|
package/register/update.test.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { CHAIN_ID, getIdentityRegistryAddress } from "@aixyz/erc-8004";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
deriveAgentUri,
|
|
5
|
+
isTTY,
|
|
6
|
+
parseSupportedTrust,
|
|
7
|
+
promptAgentUrl,
|
|
8
|
+
promptRegistryAddress,
|
|
9
|
+
promptSupportedTrust,
|
|
10
|
+
withTTY,
|
|
11
|
+
} from "./utils/prompt";
|
|
12
|
+
import { selectChain } from "./utils/chain";
|
|
4
13
|
|
|
5
14
|
describe("update command chain configuration", () => {
|
|
6
15
|
test("sepolia chain ID is correct", () => {
|
|
@@ -45,3 +54,112 @@ describe("deriveAgentUri", () => {
|
|
|
45
54
|
);
|
|
46
55
|
});
|
|
47
56
|
});
|
|
57
|
+
|
|
58
|
+
describe("parseSupportedTrust", () => {
|
|
59
|
+
test("parses a single value", () => {
|
|
60
|
+
expect(parseSupportedTrust("reputation")).toStrictEqual(["reputation"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("parses multiple comma-separated values", () => {
|
|
64
|
+
expect(parseSupportedTrust("reputation,tee-attestation")).toStrictEqual(["reputation", "tee-attestation"]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("trims whitespace around values", () => {
|
|
68
|
+
expect(parseSupportedTrust("reputation, tee-attestation , social")).toStrictEqual([
|
|
69
|
+
"reputation",
|
|
70
|
+
"tee-attestation",
|
|
71
|
+
"social",
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("throws for invalid trust mechanism", () => {
|
|
76
|
+
expect(() => parseSupportedTrust("reputation,invalid")).toThrow("Invalid trust mechanism(s): invalid");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("throws for empty string", () => {
|
|
80
|
+
expect(() => parseSupportedTrust("")).toThrow("--supported-trust requires at least one value");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("accepts all valid mechanisms", () => {
|
|
84
|
+
expect(parseSupportedTrust("reputation,crypto-economic,tee-attestation,social,governance")).toStrictEqual([
|
|
85
|
+
"reputation",
|
|
86
|
+
"crypto-economic",
|
|
87
|
+
"tee-attestation",
|
|
88
|
+
"social",
|
|
89
|
+
"governance",
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("isTTY", () => {
|
|
95
|
+
test("returns a boolean", () => {
|
|
96
|
+
expect(typeof isTTY()).toBe("boolean");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("withTTY", () => {
|
|
101
|
+
async function withNoTTYOverride<T>(fn: () => Promise<T>): Promise<T> {
|
|
102
|
+
const originalIsTTY = process.stdin.isTTY;
|
|
103
|
+
(process.stdin as NodeJS.ReadStream & { isTTY: boolean | undefined }).isTTY = undefined;
|
|
104
|
+
try {
|
|
105
|
+
return await fn();
|
|
106
|
+
} finally {
|
|
107
|
+
(process.stdin as NodeJS.ReadStream & { isTTY: boolean | undefined }).isTTY = originalIsTTY;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
test("throws the provided message when no TTY", async () => {
|
|
112
|
+
await withNoTTYOverride(async () => {
|
|
113
|
+
await expect(withTTY(() => Promise.resolve("ok"), "use --flag instead")).rejects.toThrow("use --flag instead");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("returns the fn result when TTY is available", async () => {
|
|
118
|
+
const originalIsTTY = process.stdin.isTTY;
|
|
119
|
+
(process.stdin as NodeJS.ReadStream & { isTTY: boolean | undefined }).isTTY = true;
|
|
120
|
+
try {
|
|
121
|
+
const result = await withTTY(() => Promise.resolve("hello"), "should not throw");
|
|
122
|
+
expect(result).toBe("hello");
|
|
123
|
+
} finally {
|
|
124
|
+
(process.stdin as NodeJS.ReadStream & { isTTY: boolean | undefined }).isTTY = originalIsTTY;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("TTY-gated prompts throw in non-interactive environments", () => {
|
|
130
|
+
// Tests run in a piped (non-TTY) environment, so these error paths are exercised directly.
|
|
131
|
+
|
|
132
|
+
async function withNoTTY<T>(fn: () => Promise<T>): Promise<T> {
|
|
133
|
+
const originalIsTTY = process.stdin.isTTY;
|
|
134
|
+
(process.stdin as NodeJS.ReadStream & { isTTY: boolean | undefined }).isTTY = undefined;
|
|
135
|
+
try {
|
|
136
|
+
return await fn();
|
|
137
|
+
} finally {
|
|
138
|
+
(process.stdin as NodeJS.ReadStream & { isTTY: boolean | undefined }).isTTY = originalIsTTY;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
test("promptAgentUrl throws when stdin is not a TTY", async () => {
|
|
143
|
+
await withNoTTY(async () => {
|
|
144
|
+
await expect(promptAgentUrl()).rejects.toThrow("No TTY detected. Provide --url");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("promptSupportedTrust throws when stdin is not a TTY", async () => {
|
|
149
|
+
await withNoTTY(async () => {
|
|
150
|
+
await expect(promptSupportedTrust()).rejects.toThrow("No TTY detected");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("promptRegistryAddress throws when stdin is not a TTY", async () => {
|
|
155
|
+
await withNoTTY(async () => {
|
|
156
|
+
await expect(promptRegistryAddress()).rejects.toThrow("No TTY detected. Provide --registry");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("selectChain throws when stdin is not a TTY", async () => {
|
|
161
|
+
await withNoTTY(async () => {
|
|
162
|
+
await expect(selectChain()).rejects.toThrow("No TTY detected. Provide --chain-id");
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
package/register/update.ts
CHANGED
|
@@ -2,10 +2,10 @@ 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 {
|
|
5
|
+
import { resolveChainConfigById, validateBrowserRpcConflict, getExplorerUrl, CHAINS } from "./utils/chain";
|
|
6
6
|
import { writeResultJson } from "./utils/result";
|
|
7
7
|
import { label, truncateUri, broadcastAndConfirm, logSignResult } from "./utils/transaction";
|
|
8
|
-
import { promptAgentUrl, promptSelectRegistration, deriveAgentUri } from "./utils/prompt";
|
|
8
|
+
import { promptAgentUrl, promptSelectRegistration, deriveAgentUri, isTTY } from "./utils/prompt";
|
|
9
9
|
import { readRegistrations } from "./utils/erc8004-file";
|
|
10
10
|
import { confirm } from "@inquirer/prompts";
|
|
11
11
|
import chalk from "chalk";
|
|
@@ -14,6 +14,7 @@ import type { BaseOptions } from "./index";
|
|
|
14
14
|
|
|
15
15
|
export interface UpdateOptions extends BaseOptions {
|
|
16
16
|
url?: string;
|
|
17
|
+
agentId?: number;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export async function update(options: UpdateOptions): Promise<void> {
|
|
@@ -25,7 +26,18 @@ export async function update(options: UpdateOptions): Promise<void> {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
// Step 2: Select which registration to update
|
|
28
|
-
|
|
29
|
+
let selected: (typeof registrations)[number];
|
|
30
|
+
if (options.agentId !== undefined) {
|
|
31
|
+
const match = registrations.find((r) => r.agentId === options.agentId);
|
|
32
|
+
if (!match) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`No registration found with agentId ${options.agentId}. Available: ${registrations.map((r) => r.agentId).join(", ")}`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
selected = match;
|
|
38
|
+
} else {
|
|
39
|
+
selected = await promptSelectRegistration(registrations);
|
|
40
|
+
}
|
|
29
41
|
|
|
30
42
|
// Step 3: Derive chain info from agentRegistry (eip155:<chainId>:<address>)
|
|
31
43
|
const parts = selected.agentRegistry.split(":");
|
|
@@ -36,18 +48,20 @@ export async function update(options: UpdateOptions): Promise<void> {
|
|
|
36
48
|
const chainId = Number(parts[1]);
|
|
37
49
|
const registryAddress = parts.slice(2).join(":") as `0x${string}`;
|
|
38
50
|
const chainName = Object.entries(CHAINS).find(([, config]) => config.chainId === chainId)?.[0] ?? `chain-${chainId}`;
|
|
39
|
-
const chainConfig =
|
|
51
|
+
const chainConfig = resolveChainConfigById(chainId, options.rpcUrl);
|
|
40
52
|
|
|
41
53
|
// Step 4: Get new agent URL and derive URI
|
|
42
54
|
const agentUrl = options.url ?? (await promptAgentUrl());
|
|
43
55
|
const resolvedUri = deriveAgentUri(agentUrl);
|
|
44
56
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
if (isTTY()) {
|
|
58
|
+
const yes = await confirm({
|
|
59
|
+
message: `Will update URI to: ${chalk.cyan(resolvedUri)} — confirm?`,
|
|
60
|
+
default: true,
|
|
61
|
+
});
|
|
62
|
+
if (!yes) {
|
|
63
|
+
throw new Error("Aborted.");
|
|
64
|
+
}
|
|
51
65
|
}
|
|
52
66
|
|
|
53
67
|
// Step 5: Encode transaction
|
package/register/utils/chain.ts
CHANGED
|
@@ -1,20 +1,129 @@
|
|
|
1
|
-
import { isAddress, type Chain } from "viem";
|
|
2
|
-
import {
|
|
1
|
+
import { isAddress, defineChain, type Chain } from "viem";
|
|
2
|
+
import {
|
|
3
|
+
abstract,
|
|
4
|
+
abstractTestnet,
|
|
5
|
+
arbitrum,
|
|
6
|
+
arbitrumSepolia,
|
|
7
|
+
avalanche,
|
|
8
|
+
avalancheFuji,
|
|
9
|
+
base,
|
|
10
|
+
baseSepolia,
|
|
11
|
+
bsc,
|
|
12
|
+
bscTestnet,
|
|
13
|
+
celo,
|
|
14
|
+
celoSepolia,
|
|
15
|
+
foundry,
|
|
16
|
+
gnosis,
|
|
17
|
+
linea,
|
|
18
|
+
lineaSepolia,
|
|
19
|
+
mainnet,
|
|
20
|
+
mantle,
|
|
21
|
+
mantleSepoliaTestnet,
|
|
22
|
+
megaeth,
|
|
23
|
+
monad,
|
|
24
|
+
monadTestnet,
|
|
25
|
+
optimism,
|
|
26
|
+
optimismSepolia,
|
|
27
|
+
polygon,
|
|
28
|
+
polygonAmoy,
|
|
29
|
+
scroll,
|
|
30
|
+
scrollSepolia,
|
|
31
|
+
sepolia,
|
|
32
|
+
taiko,
|
|
33
|
+
} from "viem/chains";
|
|
3
34
|
import { CHAIN_ID, getIdentityRegistryAddress } from "@aixyz/erc-8004";
|
|
4
|
-
import { select } from "@inquirer/prompts";
|
|
35
|
+
import { input, select } from "@inquirer/prompts";
|
|
36
|
+
import { withTTY } from "./prompt";
|
|
5
37
|
|
|
6
38
|
export interface ChainConfig {
|
|
7
39
|
chain: Chain;
|
|
8
40
|
chainId: number;
|
|
9
41
|
}
|
|
10
42
|
|
|
43
|
+
// Maps supported chain IDs to viem Chain objects (derived from @aixyz/erc-8004 CHAIN_ID)
|
|
44
|
+
const VIEM_CHAIN_BY_ID: Record<number, Chain> = {
|
|
45
|
+
[CHAIN_ID.ABSTRACT]: abstract,
|
|
46
|
+
[CHAIN_ID.ARBITRUM]: arbitrum,
|
|
47
|
+
[CHAIN_ID.AVALANCHE]: avalanche,
|
|
48
|
+
[CHAIN_ID.BASE]: base,
|
|
49
|
+
[CHAIN_ID.BSC]: bsc,
|
|
50
|
+
[CHAIN_ID.CELO]: celo,
|
|
51
|
+
[CHAIN_ID.GNOSIS]: gnosis,
|
|
52
|
+
[CHAIN_ID.LINEA]: linea,
|
|
53
|
+
[CHAIN_ID.MAINNET]: mainnet,
|
|
54
|
+
[CHAIN_ID.MANTLE]: mantle,
|
|
55
|
+
[CHAIN_ID.MEGAETH]: megaeth,
|
|
56
|
+
[CHAIN_ID.MONAD]: monad,
|
|
57
|
+
[CHAIN_ID.OPTIMISM]: optimism,
|
|
58
|
+
[CHAIN_ID.POLYGON]: polygon,
|
|
59
|
+
[CHAIN_ID.SCROLL]: scroll,
|
|
60
|
+
[CHAIN_ID.TAIKO]: taiko,
|
|
61
|
+
[CHAIN_ID.ABSTRACT_TESTNET]: abstractTestnet,
|
|
62
|
+
[CHAIN_ID.ARBITRUM_SEPOLIA]: arbitrumSepolia,
|
|
63
|
+
[CHAIN_ID.AVALANCHE_FUJI]: avalancheFuji,
|
|
64
|
+
[CHAIN_ID.BASE_SEPOLIA]: baseSepolia,
|
|
65
|
+
[CHAIN_ID.BSC_TESTNET]: bscTestnet,
|
|
66
|
+
[CHAIN_ID.CELO_SEPOLIA]: celoSepolia,
|
|
67
|
+
[CHAIN_ID.LINEA_SEPOLIA]: lineaSepolia,
|
|
68
|
+
[CHAIN_ID.MANTLE_SEPOLIA]: mantleSepoliaTestnet,
|
|
69
|
+
[CHAIN_ID.MONAD_TESTNET]: monadTestnet,
|
|
70
|
+
[CHAIN_ID.OPTIMISM_SEPOLIA]: optimismSepolia,
|
|
71
|
+
[CHAIN_ID.POLYGON_AMOY]: polygonAmoy,
|
|
72
|
+
[CHAIN_ID.SCROLL_SEPOLIA]: scrollSepolia,
|
|
73
|
+
[CHAIN_ID.SEPOLIA]: sepolia,
|
|
74
|
+
31337: foundry,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Build CHAINS from CHAIN_ID as the single source of truth from @aixyz/erc-8004.
|
|
78
|
+
// Chain names are derived from CHAIN_ID keys: lowercase with underscores replaced by hyphens.
|
|
11
79
|
export const CHAINS: Record<string, ChainConfig> = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
80
|
+
...Object.fromEntries(
|
|
81
|
+
(Object.entries(CHAIN_ID) as [string, number][])
|
|
82
|
+
.filter(([, id]) => id in VIEM_CHAIN_BY_ID)
|
|
83
|
+
.map(([key, id]) => [key.toLowerCase().replace(/_/g, "-"), { chain: VIEM_CHAIN_BY_ID[id]!, chainId: id }]),
|
|
84
|
+
),
|
|
15
85
|
localhost: { chain: foundry, chainId: 31337 },
|
|
16
86
|
};
|
|
17
87
|
|
|
88
|
+
// Priority-ordered chain IDs for interactive selection — most popular first.
|
|
89
|
+
const CHAIN_SELECTION_ORDER: number[] = [
|
|
90
|
+
// Popular mainnets
|
|
91
|
+
CHAIN_ID.MAINNET,
|
|
92
|
+
CHAIN_ID.BASE,
|
|
93
|
+
CHAIN_ID.ARBITRUM,
|
|
94
|
+
CHAIN_ID.OPTIMISM,
|
|
95
|
+
CHAIN_ID.POLYGON,
|
|
96
|
+
CHAIN_ID.BSC,
|
|
97
|
+
CHAIN_ID.AVALANCHE,
|
|
98
|
+
CHAIN_ID.SCROLL,
|
|
99
|
+
CHAIN_ID.LINEA,
|
|
100
|
+
CHAIN_ID.CELO,
|
|
101
|
+
CHAIN_ID.GNOSIS,
|
|
102
|
+
CHAIN_ID.TAIKO,
|
|
103
|
+
CHAIN_ID.MANTLE,
|
|
104
|
+
CHAIN_ID.MONAD,
|
|
105
|
+
CHAIN_ID.MEGAETH,
|
|
106
|
+
CHAIN_ID.ABSTRACT,
|
|
107
|
+
// Popular testnets
|
|
108
|
+
CHAIN_ID.SEPOLIA,
|
|
109
|
+
CHAIN_ID.BASE_SEPOLIA,
|
|
110
|
+
CHAIN_ID.ARBITRUM_SEPOLIA,
|
|
111
|
+
CHAIN_ID.OPTIMISM_SEPOLIA,
|
|
112
|
+
CHAIN_ID.POLYGON_AMOY,
|
|
113
|
+
CHAIN_ID.AVALANCHE_FUJI,
|
|
114
|
+
CHAIN_ID.BSC_TESTNET,
|
|
115
|
+
CHAIN_ID.SCROLL_SEPOLIA,
|
|
116
|
+
CHAIN_ID.LINEA_SEPOLIA,
|
|
117
|
+
CHAIN_ID.CELO_SEPOLIA,
|
|
118
|
+
CHAIN_ID.MANTLE_SEPOLIA,
|
|
119
|
+
CHAIN_ID.MONAD_TESTNET,
|
|
120
|
+
CHAIN_ID.ABSTRACT_TESTNET,
|
|
121
|
+
// Special
|
|
122
|
+
31337,
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
const OTHER_CHAIN_ID = -1;
|
|
126
|
+
|
|
18
127
|
export function resolveChainConfig(chainName: string): ChainConfig {
|
|
19
128
|
const config = CHAINS[chainName];
|
|
20
129
|
if (!config) {
|
|
@@ -23,24 +132,75 @@ export function resolveChainConfig(chainName: string): ChainConfig {
|
|
|
23
132
|
return config;
|
|
24
133
|
}
|
|
25
134
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
135
|
+
// Resolve chain config by numeric chain ID, supporting BYO chains via rpcUrl.
|
|
136
|
+
// For known chain IDs the viem chain object is used directly.
|
|
137
|
+
// For unknown chain IDs, a minimal chain is constructed using defineChain (requires rpcUrl).
|
|
138
|
+
export function resolveChainConfigById(chainId: number, rpcUrl?: string): ChainConfig {
|
|
139
|
+
const chain = VIEM_CHAIN_BY_ID[chainId];
|
|
140
|
+
if (chain) {
|
|
141
|
+
return { chain, chainId };
|
|
142
|
+
}
|
|
143
|
+
if (!rpcUrl) {
|
|
144
|
+
throw new Error(`Unknown chain ID ${chainId}. Provide --rpc-url to register on a custom chain.`);
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
chain: defineChain({
|
|
148
|
+
id: chainId,
|
|
149
|
+
name: `chain-${chainId}`,
|
|
150
|
+
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
|
151
|
+
rpcUrls: { default: { http: [rpcUrl] } },
|
|
152
|
+
}),
|
|
153
|
+
chainId,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Prompt user to select a chain interactively, returning the numeric chain ID.
|
|
158
|
+
// Chains are sorted by popularity. "Other" allows entering any custom chain ID.
|
|
159
|
+
export async function selectChain(): Promise<number> {
|
|
160
|
+
return withTTY(async () => {
|
|
161
|
+
const chainById = new Map(Object.values(CHAINS).map((c) => [c.chainId, c]));
|
|
162
|
+
const nameById = new Map(Object.entries(CHAINS).map(([name, c]) => [c.chainId, name]));
|
|
163
|
+
|
|
164
|
+
const choices = CHAIN_SELECTION_ORDER.filter((id) => chainById.has(id)).map((id) => ({
|
|
165
|
+
name: `${nameById.get(id) ?? `chain-${id}`} (${id})`,
|
|
166
|
+
value: id,
|
|
167
|
+
}));
|
|
168
|
+
choices.push({ name: "Other (enter chain ID)", value: OTHER_CHAIN_ID });
|
|
169
|
+
|
|
170
|
+
const selected = await select({ message: "Select target chain:", choices });
|
|
171
|
+
|
|
172
|
+
if (selected === OTHER_CHAIN_ID) {
|
|
173
|
+
const raw = await input({
|
|
174
|
+
message: "Enter chain ID:",
|
|
175
|
+
validate: (v) => {
|
|
176
|
+
const n = parseInt(v, 10);
|
|
177
|
+
return Number.isInteger(n) && n > 0 ? true : "Must be a positive integer";
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
return parseInt(raw, 10);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return selected;
|
|
184
|
+
}, "No TTY detected. Provide --chain-id to specify the target chain.");
|
|
31
185
|
}
|
|
32
186
|
|
|
33
|
-
|
|
187
|
+
// Returns the registry address if known, or null if no default exists for the chain (requires interactive prompt).
|
|
188
|
+
// Throws only for an explicitly invalid registry address.
|
|
189
|
+
export function resolveRegistryAddress(chainId: number, registry?: string): `0x${string}` | null {
|
|
34
190
|
if (registry) {
|
|
35
191
|
if (!isAddress(registry)) {
|
|
36
192
|
throw new Error(`Invalid registry address: ${registry}`);
|
|
37
193
|
}
|
|
38
194
|
return registry as `0x${string}`;
|
|
39
195
|
}
|
|
40
|
-
if (
|
|
41
|
-
|
|
196
|
+
if (chainId === 31337) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
return getIdentityRegistryAddress(chainId) as `0x${string}`;
|
|
201
|
+
} catch {
|
|
202
|
+
return null;
|
|
42
203
|
}
|
|
43
|
-
return getIdentityRegistryAddress(chainId) as `0x${string}`;
|
|
44
204
|
}
|
|
45
205
|
|
|
46
206
|
export function validateBrowserRpcConflict(browser: boolean | undefined, rpcUrl: string | undefined): void {
|
package/register/utils/prompt.ts
CHANGED
|
@@ -1,40 +1,82 @@
|
|
|
1
1
|
import { checkbox, confirm, input, select } from "@inquirer/prompts";
|
|
2
|
+
import { isAddress } from "viem";
|
|
2
3
|
import type { RegistrationEntry } from "@aixyz/erc-8004/schemas/registration";
|
|
3
4
|
|
|
5
|
+
export function isTTY(): boolean {
|
|
6
|
+
return Boolean(process.stdin.isTTY);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function withTTY<T>(fn: () => Promise<T>, nonTtyMessage: string): Promise<T> {
|
|
10
|
+
if (!isTTY()) {
|
|
11
|
+
throw new Error(nonTtyMessage);
|
|
12
|
+
}
|
|
13
|
+
return fn();
|
|
14
|
+
}
|
|
15
|
+
|
|
4
16
|
export async function promptAgentUrl(): Promise<string> {
|
|
5
|
-
return
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
return withTTY(
|
|
18
|
+
() =>
|
|
19
|
+
input({
|
|
20
|
+
message: "Agent deployment URL (e.g., https://my-agent.example.com):",
|
|
21
|
+
validate: (value) => {
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(value);
|
|
24
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
25
|
+
return "URL must start with https:// or http://";
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
} catch {
|
|
29
|
+
return "Must be a valid URL (e.g., https://my-agent.example.com)";
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
"No TTY detected. Provide --url to specify the agent deployment URL.",
|
|
34
|
+
);
|
|
19
35
|
}
|
|
20
36
|
|
|
21
37
|
export async function promptSupportedTrust(): Promise<string[]> {
|
|
22
|
-
return
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
return withTTY(
|
|
39
|
+
() =>
|
|
40
|
+
checkbox({
|
|
41
|
+
message: "Select supported trust mechanisms:",
|
|
42
|
+
choices: [
|
|
43
|
+
{ name: "reputation", value: "reputation", checked: true },
|
|
44
|
+
{ name: "crypto-economic", value: "crypto-economic" },
|
|
45
|
+
{ name: "tee-attestation", value: "tee-attestation" },
|
|
46
|
+
{ name: "social", value: "social" },
|
|
47
|
+
{ name: "governance", value: "governance" },
|
|
48
|
+
],
|
|
49
|
+
required: true,
|
|
50
|
+
}),
|
|
51
|
+
`No TTY detected. Provide --supported-trust to specify trust mechanisms (e.g., --supported-trust "reputation,tee-attestation").`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const VALID_TRUST_MECHANISMS = ["reputation", "crypto-economic", "tee-attestation", "social", "governance"] as const;
|
|
56
|
+
|
|
57
|
+
export function parseSupportedTrust(value: string): string[] {
|
|
58
|
+
const items = value
|
|
59
|
+
.split(",")
|
|
60
|
+
.map((s) => s.trim())
|
|
61
|
+
.filter(Boolean);
|
|
62
|
+
const invalid = items.filter((i) => !(VALID_TRUST_MECHANISMS as readonly string[]).includes(i));
|
|
63
|
+
if (invalid.length > 0) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Invalid trust mechanism(s): ${invalid.join(", ")}. Valid values: ${VALID_TRUST_MECHANISMS.join(", ")}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (items.length === 0) {
|
|
69
|
+
throw new Error("--supported-trust requires at least one value.");
|
|
70
|
+
}
|
|
71
|
+
return items;
|
|
33
72
|
}
|
|
34
73
|
|
|
35
74
|
export async function promptSelectRegistration(registrations: RegistrationEntry[]): Promise<RegistrationEntry> {
|
|
36
75
|
if (registrations.length === 1) {
|
|
37
76
|
const reg = registrations[0]!;
|
|
77
|
+
if (!isTTY()) {
|
|
78
|
+
return reg;
|
|
79
|
+
}
|
|
38
80
|
const yes = await confirm({
|
|
39
81
|
message: `Update this registration? (agentId: ${reg.agentId}, registry: ${reg.agentRegistry})`,
|
|
40
82
|
default: true,
|
|
@@ -45,13 +87,27 @@ export async function promptSelectRegistration(registrations: RegistrationEntry[
|
|
|
45
87
|
return reg;
|
|
46
88
|
}
|
|
47
89
|
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
90
|
+
return withTTY(
|
|
91
|
+
() =>
|
|
92
|
+
select({
|
|
93
|
+
message: "Select registration to update:",
|
|
94
|
+
choices: registrations.map((reg) => ({
|
|
95
|
+
name: `agentId: ${reg.agentId} — ${reg.agentRegistry}`,
|
|
96
|
+
value: reg,
|
|
97
|
+
})),
|
|
98
|
+
}),
|
|
99
|
+
"No TTY detected. Multiple registrations found in app/erc-8004.ts; cannot select one non-interactively.",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function promptRegistryAddress(): Promise<`0x${string}`> {
|
|
104
|
+
return withTTY(async () => {
|
|
105
|
+
const value = await input({
|
|
106
|
+
message: "IdentityRegistry contract address (no default for this chain):",
|
|
107
|
+
validate: (v) => (isAddress(v) ? true : "Must be a valid Ethereum address (0x…)"),
|
|
108
|
+
});
|
|
109
|
+
return value as `0x${string}`;
|
|
110
|
+
}, "No TTY detected. Provide --registry to specify the IdentityRegistry contract address.");
|
|
55
111
|
}
|
|
56
112
|
|
|
57
113
|
export function deriveAgentUri(url: string): string {
|
package/register/wallet/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import type { Chain, WalletClient } from "viem";
|
|
3
3
|
import { select, input, password } from "@inquirer/prompts";
|
|
4
|
+
import { withTTY } from "../utils/prompt";
|
|
4
5
|
import { createPrivateKeyWallet } from "./privatekey";
|
|
5
6
|
import { createKeystoreWallet } from "./keystore";
|
|
6
7
|
|
|
@@ -33,36 +34,38 @@ export async function selectWalletMethod(options: WalletOptions): Promise<Wallet
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
// Interactive: prompt user to choose
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
return withTTY(async () => {
|
|
38
|
+
const method = await select({
|
|
39
|
+
message: "Select signing method:",
|
|
40
|
+
choices: [
|
|
41
|
+
{ name: "Keystore file", value: "keystore" },
|
|
42
|
+
{ name: "Browser wallet (any EIP-6963 compatible wallets)", value: "browser" },
|
|
43
|
+
{ name: "Private key (not recommended)", value: "privatekey" },
|
|
44
|
+
],
|
|
45
|
+
});
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
switch (method) {
|
|
48
|
+
case "keystore": {
|
|
49
|
+
const keystorePath = await input({
|
|
50
|
+
message: "Enter keystore path:",
|
|
51
|
+
default: `${homedir()}/.foundry/keystores/default`,
|
|
52
|
+
});
|
|
53
|
+
return { type: "keystore", path: keystorePath };
|
|
54
|
+
}
|
|
55
|
+
case "browser":
|
|
56
|
+
return { type: "browser" };
|
|
57
|
+
case "privatekey": {
|
|
58
|
+
const key = await password({
|
|
59
|
+
message: "Enter private key:",
|
|
60
|
+
mask: "*",
|
|
61
|
+
});
|
|
62
|
+
console.warn("Warning: Using raw private key is not recommended for production");
|
|
63
|
+
return { type: "privatekey", resolveKey: () => Promise.resolve(key) };
|
|
64
|
+
}
|
|
65
|
+
default:
|
|
66
|
+
throw new Error("No wallet method selected");
|
|
52
67
|
}
|
|
53
|
-
|
|
54
|
-
return { type: "browser" };
|
|
55
|
-
case "privatekey": {
|
|
56
|
-
const key = await password({
|
|
57
|
-
message: "Enter private key:",
|
|
58
|
-
mask: "*",
|
|
59
|
-
});
|
|
60
|
-
console.warn("Warning: Using raw private key is not recommended for production");
|
|
61
|
-
return { type: "privatekey", resolveKey: () => Promise.resolve(key) };
|
|
62
|
-
}
|
|
63
|
-
default:
|
|
64
|
-
throw new Error("No wallet method selected");
|
|
65
|
-
}
|
|
68
|
+
}, "No TTY detected. Provide --keystore, --browser, or PRIVATE_KEY environment variable to specify a signing method.");
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
export async function createWalletFromMethod(
|