@aixyz/cli 0.6.0 → 0.8.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 +175 -0
- package/build/AixyzConfigPlugin.ts +1 -1
- package/build/AixyzServerPlugin.ts +33 -9
- package/build/icons.ts +64 -0
- package/build/index.ts +11 -8
- package/dev/index.ts +1 -1
- package/package.json +12 -6
- package/register/README.md +101 -0
- package/register/index.ts +8 -0
- package/register/register.test.ts +75 -0
- package/register/register.ts +178 -0
- package/register/set-agent-uri.test.ts +156 -0
- package/register/set-agent-uri.ts +192 -0
- package/register/utils/chain.ts +57 -0
- package/register/utils/prompt.ts +18 -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 +154 -0
- package/register/utils.ts +55 -0
- package/register/wallet/browser.test.ts +106 -0
- package/register/wallet/browser.ts +748 -0
- package/register/wallet/index.test.ts +80 -0
- package/register/wallet/index.ts +84 -0
- package/register/wallet/keystore.test.ts +63 -0
- package/register/wallet/keystore.ts +33 -0
- package/register/wallet/privatekey.test.ts +30 -0
- package/register/wallet/privatekey.ts +14 -0
- package/register/wallet/sign.test.ts +78 -0
- package/register/wallet/sign.ts +109 -0
|
@@ -0,0 +1,178 @@
|
|
|
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 { resolveUri } from "./utils";
|
|
6
|
+
import {
|
|
7
|
+
resolveChainConfig,
|
|
8
|
+
selectChain,
|
|
9
|
+
resolveRegistryAddress,
|
|
10
|
+
validateBrowserRpcConflict,
|
|
11
|
+
getExplorerUrl,
|
|
12
|
+
} from "./utils/chain";
|
|
13
|
+
import { writeResultJson } from "./utils/result";
|
|
14
|
+
import { label, truncateUri, broadcastAndConfirm, logSignResult } from "./utils/transaction";
|
|
15
|
+
import chalk from "chalk";
|
|
16
|
+
import boxen from "boxen";
|
|
17
|
+
import type { BaseOptions } from "./index";
|
|
18
|
+
|
|
19
|
+
export interface RegisterOptions extends BaseOptions {
|
|
20
|
+
uri?: string;
|
|
21
|
+
chain?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function register(options: RegisterOptions): Promise<void> {
|
|
25
|
+
const chainName = options.chain ?? (await selectChain());
|
|
26
|
+
const chainConfig = resolveChainConfig(chainName);
|
|
27
|
+
|
|
28
|
+
const resolvedUri = options.uri ? resolveUri(options.uri) : undefined;
|
|
29
|
+
if (options.uri && resolvedUri !== options.uri) {
|
|
30
|
+
console.log(`Resolved ${options.uri} to data URI (${resolvedUri!.length} chars)`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const registryAddress = resolveRegistryAddress(chainName, chainConfig.chainId, options.registry);
|
|
34
|
+
|
|
35
|
+
const data = encodeFunctionData({
|
|
36
|
+
abi: IdentityRegistryAbi,
|
|
37
|
+
functionName: "register",
|
|
38
|
+
args: resolvedUri ? [resolvedUri] : [],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const printTxDetails = (header: string) => {
|
|
42
|
+
console.log("");
|
|
43
|
+
console.log(chalk.dim(header));
|
|
44
|
+
console.log(` ${label("To")}${registryAddress}`);
|
|
45
|
+
console.log(` ${label("Data")}${data.slice(0, 10)}${chalk.dim("\u2026" + (data.length - 2) / 2 + " bytes")}`);
|
|
46
|
+
console.log(` ${label("Chain")}${chainName}`);
|
|
47
|
+
console.log(` ${label("Function")}${resolvedUri ? "register(string memory agentURI)" : "register()"}`);
|
|
48
|
+
if (resolvedUri) {
|
|
49
|
+
console.log(` ${label("URI")}${truncateUri(resolvedUri)}`);
|
|
50
|
+
}
|
|
51
|
+
console.log("");
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
validateBrowserRpcConflict(options.browser, options.rpcUrl);
|
|
55
|
+
|
|
56
|
+
if (!options.broadcast) {
|
|
57
|
+
if (options.browser || options.keystore || process.env.PRIVATE_KEY) {
|
|
58
|
+
console.warn("Note: --browser/--keystore/PRIVATE_KEY ignored in dry-run mode. Pass --broadcast to use a wallet.");
|
|
59
|
+
}
|
|
60
|
+
printTxDetails("Transaction details (dry-run)");
|
|
61
|
+
console.log("Dry-run complete. To sign and broadcast, re-run with --broadcast.");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const walletMethod = await selectWalletMethod(options);
|
|
66
|
+
validateBrowserRpcConflict(walletMethod.type === "browser" || undefined, options.rpcUrl);
|
|
67
|
+
|
|
68
|
+
printTxDetails("Signing transaction...");
|
|
69
|
+
|
|
70
|
+
const result = await signTransaction({
|
|
71
|
+
walletMethod,
|
|
72
|
+
tx: { to: registryAddress, data },
|
|
73
|
+
chain: chainConfig.chain,
|
|
74
|
+
rpcUrl: options.rpcUrl,
|
|
75
|
+
options: {
|
|
76
|
+
browser: { chainId: chainConfig.chainId, chainName, uri: resolvedUri },
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
logSignResult(walletMethod.type, result);
|
|
80
|
+
|
|
81
|
+
const { hash, receipt, timestamp } = await broadcastAndConfirm({
|
|
82
|
+
result,
|
|
83
|
+
chain: chainConfig.chain,
|
|
84
|
+
rpcUrl: options.rpcUrl,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const resultData = printResult(receipt, timestamp, chainConfig.chain, chainConfig.chainId, hash);
|
|
88
|
+
|
|
89
|
+
if (options.outDir) {
|
|
90
|
+
writeResultJson(options.outDir, "registration", resultData);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface RegistrationResult {
|
|
95
|
+
agentId?: string;
|
|
96
|
+
owner?: string;
|
|
97
|
+
uri?: string;
|
|
98
|
+
chainId: number;
|
|
99
|
+
block: string;
|
|
100
|
+
timestamp: string;
|
|
101
|
+
gasPaid: string;
|
|
102
|
+
nativeCurrency: string;
|
|
103
|
+
txHash: string;
|
|
104
|
+
explorer?: string;
|
|
105
|
+
metadata?: Record<string, string>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function printResult(
|
|
109
|
+
receipt: { blockNumber: bigint; gasUsed: bigint; effectiveGasPrice: bigint; logs: Log[] },
|
|
110
|
+
timestamp: bigint,
|
|
111
|
+
chain: Chain,
|
|
112
|
+
chainId: number,
|
|
113
|
+
hash: `0x${string}`,
|
|
114
|
+
): RegistrationResult {
|
|
115
|
+
const events = parseEventLogs({ abi: IdentityRegistryAbi, logs: receipt.logs });
|
|
116
|
+
const registered = events.find((e) => e.eventName === "Registered");
|
|
117
|
+
const metadataEvents = events.filter((e) => e.eventName === "MetadataSet");
|
|
118
|
+
|
|
119
|
+
const lines: string[] = [];
|
|
120
|
+
const result: RegistrationResult = {
|
|
121
|
+
chainId,
|
|
122
|
+
block: receipt.blockNumber.toString(),
|
|
123
|
+
timestamp: new Date(Number(timestamp) * 1000).toUTCString(),
|
|
124
|
+
gasPaid: formatEther(receipt.gasUsed * receipt.effectiveGasPrice),
|
|
125
|
+
nativeCurrency: chain.nativeCurrency?.symbol ?? "ETH",
|
|
126
|
+
txHash: hash,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (registered) {
|
|
130
|
+
const { agentId, agentURI, owner } = registered.args as { agentId: bigint; agentURI: string; owner: string };
|
|
131
|
+
result.agentId = agentId.toString();
|
|
132
|
+
result.owner = owner;
|
|
133
|
+
if (agentURI) result.uri = agentURI;
|
|
134
|
+
|
|
135
|
+
lines.push(`${label("Agent ID")}${chalk.bold(result.agentId)}`);
|
|
136
|
+
lines.push(`${label("Owner")}${owner}`);
|
|
137
|
+
if (agentURI) {
|
|
138
|
+
lines.push(`${label("URI")}${truncateUri(agentURI)}`);
|
|
139
|
+
}
|
|
140
|
+
lines.push(`${label("Block")}${receipt.blockNumber}`);
|
|
141
|
+
} else {
|
|
142
|
+
lines.push(`${label("Block")}${receipt.blockNumber}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
lines.push(`${label("Timestamp")}${result.timestamp}`);
|
|
146
|
+
lines.push(`${label("Gas Paid")}${result.gasPaid} ${result.nativeCurrency}`);
|
|
147
|
+
lines.push(`${label("Tx Hash")}${hash}`);
|
|
148
|
+
|
|
149
|
+
const explorerUrl = getExplorerUrl(chain, hash);
|
|
150
|
+
if (explorerUrl) {
|
|
151
|
+
result.explorer = explorerUrl;
|
|
152
|
+
lines.push(`${label("Explorer")}${chalk.cyan(explorerUrl)}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (metadataEvents.length > 0) {
|
|
156
|
+
result.metadata = {};
|
|
157
|
+
lines.push("");
|
|
158
|
+
lines.push(chalk.dim("Metadata"));
|
|
159
|
+
for (const event of metadataEvents) {
|
|
160
|
+
const { metadataKey, metadataValue } = event.args as { metadataKey: string; metadataValue: string };
|
|
161
|
+
result.metadata[metadataKey] = metadataValue;
|
|
162
|
+
lines.push(`${label(metadataKey)}${metadataValue}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log("");
|
|
167
|
+
console.log(
|
|
168
|
+
boxen(lines.join("\n"), {
|
|
169
|
+
padding: { left: 1, right: 1, top: 0, bottom: 0 },
|
|
170
|
+
borderStyle: "round",
|
|
171
|
+
borderColor: "green",
|
|
172
|
+
title: "Agent registered successfully",
|
|
173
|
+
titleAlignment: "left",
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { CHAIN_ID, getIdentityRegistryAddress } from "@aixyz/erc-8004";
|
|
3
|
+
import { setAgentUri, validateAgentId } from "./set-agent-uri";
|
|
4
|
+
|
|
5
|
+
describe("set-agent-uri 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("validateAgentId", () => {
|
|
30
|
+
test("accepts 0", () => {
|
|
31
|
+
expect(() => validateAgentId("0")).not.toThrow();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("accepts positive integer", () => {
|
|
35
|
+
expect(() => validateAgentId("42")).not.toThrow();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("accepts large integer", () => {
|
|
39
|
+
expect(() => validateAgentId("999999999")).not.toThrow();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("rejects empty string", () => {
|
|
43
|
+
expect(() => validateAgentId("")).toThrow("Invalid agent ID");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("rejects whitespace-only string", () => {
|
|
47
|
+
expect(() => validateAgentId(" ")).toThrow("Invalid agent ID");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("accepts leading zeros", () => {
|
|
51
|
+
expect(() => validateAgentId("007")).not.toThrow();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("accepts single leading zero before digits", () => {
|
|
55
|
+
expect(() => validateAgentId("042")).not.toThrow();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("rejects negative number", () => {
|
|
59
|
+
expect(() => validateAgentId("-1")).toThrow("Invalid agent ID");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("rejects float", () => {
|
|
63
|
+
expect(() => validateAgentId("1.5")).toThrow("Invalid agent ID");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("rejects non-numeric string", () => {
|
|
67
|
+
expect(() => validateAgentId("abc")).toThrow("Invalid agent ID");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("rejects mixed string", () => {
|
|
71
|
+
expect(() => validateAgentId("12abc")).toThrow("Invalid agent ID");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("rejects Infinity", () => {
|
|
75
|
+
expect(() => validateAgentId("Infinity")).toThrow("Invalid agent ID");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("rejects NaN", () => {
|
|
79
|
+
expect(() => validateAgentId("NaN")).toThrow("Invalid agent ID");
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("set-agent-uri command validation", () => {
|
|
84
|
+
test("localhost requires --registry flag", async () => {
|
|
85
|
+
await expect(
|
|
86
|
+
setAgentUri({ agentId: "1", uri: "https://example.com/agent.json", chain: "localhost" }),
|
|
87
|
+
).rejects.toThrow("--registry is required for localhost");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("rejects unsupported chain", async () => {
|
|
91
|
+
await expect(
|
|
92
|
+
setAgentUri({ agentId: "1", uri: "https://example.com/agent.json", chain: "fakenet" }),
|
|
93
|
+
).rejects.toThrow("Unsupported chain: fakenet");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("rejects invalid registry address", async () => {
|
|
97
|
+
await expect(
|
|
98
|
+
setAgentUri({
|
|
99
|
+
agentId: "1",
|
|
100
|
+
uri: "https://example.com/agent.json",
|
|
101
|
+
chain: "localhost",
|
|
102
|
+
registry: "not-an-address",
|
|
103
|
+
}),
|
|
104
|
+
).rejects.toThrow("Invalid registry address: not-an-address");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("rejects --browser with --rpc-url", async () => {
|
|
108
|
+
await expect(
|
|
109
|
+
setAgentUri({
|
|
110
|
+
agentId: "0",
|
|
111
|
+
uri: "https://example.com/agent.json",
|
|
112
|
+
chain: "sepolia",
|
|
113
|
+
browser: true,
|
|
114
|
+
rpcUrl: "http://localhost:8545",
|
|
115
|
+
}),
|
|
116
|
+
).rejects.toThrow("--rpc-url cannot be used with browser wallet");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("dry-run completes without wallet interaction when --broadcast is not set", async () => {
|
|
120
|
+
await expect(
|
|
121
|
+
setAgentUri({ agentId: "1", uri: "https://example.com/agent.json", chain: "sepolia" }),
|
|
122
|
+
).resolves.toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("rejects invalid agent ID (negative)", async () => {
|
|
126
|
+
await expect(
|
|
127
|
+
setAgentUri({ agentId: "-1", uri: "https://example.com/agent.json", chain: "sepolia" }),
|
|
128
|
+
).rejects.toThrow("Invalid agent ID");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("rejects invalid agent ID (non-integer)", async () => {
|
|
132
|
+
await expect(
|
|
133
|
+
setAgentUri({ agentId: "abc", uri: "https://example.com/agent.json", chain: "sepolia" }),
|
|
134
|
+
).rejects.toThrow("Invalid agent ID");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("rejects invalid agent ID (float)", async () => {
|
|
138
|
+
await expect(
|
|
139
|
+
setAgentUri({ agentId: "1.5", uri: "https://example.com/agent.json", chain: "sepolia" }),
|
|
140
|
+
).rejects.toThrow("Invalid agent ID");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("accepts agent ID 0 as valid", async () => {
|
|
144
|
+
// Agent ID 0 passes validation — triggers a later error (browser+rpc-url conflict)
|
|
145
|
+
// proving the agent ID check did not reject it
|
|
146
|
+
await expect(
|
|
147
|
+
setAgentUri({
|
|
148
|
+
agentId: "0",
|
|
149
|
+
uri: "https://example.com/agent.json",
|
|
150
|
+
chain: "sepolia",
|
|
151
|
+
browser: true,
|
|
152
|
+
rpcUrl: "http://localhost:8545",
|
|
153
|
+
}),
|
|
154
|
+
).rejects.toThrow("--rpc-url cannot be used with browser wallet");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { encodeFunctionData, formatEther, parseEventLogs, type Chain, type Log } from "viem";
|
|
2
|
+
import { IdentityRegistryAbi } from "@aixyz/erc-8004";
|
|
3
|
+
import { selectWalletMethod } from "./wallet";
|
|
4
|
+
import { signTransaction } from "./wallet/sign";
|
|
5
|
+
import { CliError, resolveUri } from "./utils";
|
|
6
|
+
import {
|
|
7
|
+
resolveChainConfig,
|
|
8
|
+
selectChain,
|
|
9
|
+
resolveRegistryAddress,
|
|
10
|
+
validateBrowserRpcConflict,
|
|
11
|
+
getExplorerUrl,
|
|
12
|
+
} from "./utils/chain";
|
|
13
|
+
import { writeResultJson } from "./utils/result";
|
|
14
|
+
import { label, truncateUri, broadcastAndConfirm, logSignResult } from "./utils/transaction";
|
|
15
|
+
import { confirm, input } from "@inquirer/prompts";
|
|
16
|
+
import chalk from "chalk";
|
|
17
|
+
import boxen from "boxen";
|
|
18
|
+
import type { BaseOptions } from "./index";
|
|
19
|
+
import { promptAgentId, promptUri } from "./utils/prompt";
|
|
20
|
+
|
|
21
|
+
export interface SetAgentUriOptions extends BaseOptions {
|
|
22
|
+
agentId?: string;
|
|
23
|
+
uri?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function confirmEmptyUri(): Promise<void> {
|
|
27
|
+
const yes = await confirm({
|
|
28
|
+
message: "URI is empty. This will clear the agent's metadata URI. Are you sure?",
|
|
29
|
+
default: false,
|
|
30
|
+
});
|
|
31
|
+
if (!yes) {
|
|
32
|
+
throw new CliError("Aborted.");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function validateAgentId(agentId: string): void {
|
|
37
|
+
const n = Number(agentId);
|
|
38
|
+
if (agentId.trim() === "" || !Number.isInteger(n) || n < 0) {
|
|
39
|
+
throw new CliError(`Invalid agent ID: ${agentId}. Must be a non-negative integer.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function setAgentUri(options: SetAgentUriOptions): Promise<void> {
|
|
44
|
+
const chainName = options.chain ?? (await selectChain());
|
|
45
|
+
const chainConfig = resolveChainConfig(chainName);
|
|
46
|
+
|
|
47
|
+
const agentId = options.agentId ?? (await promptAgentId());
|
|
48
|
+
validateAgentId(agentId);
|
|
49
|
+
|
|
50
|
+
const uri = options.uri ?? (await promptUri());
|
|
51
|
+
if (uri === "") {
|
|
52
|
+
await confirmEmptyUri();
|
|
53
|
+
}
|
|
54
|
+
const resolvedUri = uri === "" ? "" : resolveUri(uri);
|
|
55
|
+
if (resolvedUri !== uri) {
|
|
56
|
+
console.log(`Resolved ${uri} to data URI (${resolvedUri.length} chars)`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const registryAddress = resolveRegistryAddress(chainName, chainConfig.chainId, options.registry);
|
|
60
|
+
|
|
61
|
+
const data = encodeFunctionData({
|
|
62
|
+
abi: IdentityRegistryAbi,
|
|
63
|
+
functionName: "setAgentURI",
|
|
64
|
+
args: [BigInt(agentId), resolvedUri],
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const printTxDetails = (header: string) => {
|
|
68
|
+
console.log("");
|
|
69
|
+
console.log(chalk.dim(header));
|
|
70
|
+
console.log(` ${label("To")}${registryAddress}`);
|
|
71
|
+
console.log(` ${label("Data")}${data.slice(0, 10)}${chalk.dim("\u2026" + (data.length - 2) / 2 + " bytes")}`);
|
|
72
|
+
console.log(` ${label("Chain")}${chainName}`);
|
|
73
|
+
console.log(` ${label("Function")}setAgentURI(uint256 agentId, string calldata newURI)`);
|
|
74
|
+
console.log(` ${label("Agent ID")}${agentId}`);
|
|
75
|
+
console.log(` ${label("URI")}${truncateUri(resolvedUri)}`);
|
|
76
|
+
console.log("");
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
validateBrowserRpcConflict(options.browser, options.rpcUrl);
|
|
80
|
+
|
|
81
|
+
if (!options.broadcast) {
|
|
82
|
+
if (options.browser || options.keystore || process.env.PRIVATE_KEY) {
|
|
83
|
+
console.warn("Note: --browser/--keystore/PRIVATE_KEY ignored in dry-run mode. Pass --broadcast to use a wallet.");
|
|
84
|
+
}
|
|
85
|
+
printTxDetails("Transaction details (dry-run)");
|
|
86
|
+
console.log("Dry-run complete. To sign and broadcast, re-run with --broadcast.");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const walletMethod = await selectWalletMethod(options);
|
|
91
|
+
validateBrowserRpcConflict(walletMethod.type === "browser" || undefined, options.rpcUrl);
|
|
92
|
+
|
|
93
|
+
printTxDetails("Signing transaction...");
|
|
94
|
+
|
|
95
|
+
const result = await signTransaction({
|
|
96
|
+
walletMethod,
|
|
97
|
+
tx: { to: registryAddress, data },
|
|
98
|
+
chain: chainConfig.chain,
|
|
99
|
+
rpcUrl: options.rpcUrl,
|
|
100
|
+
options: {
|
|
101
|
+
browser: { chainId: chainConfig.chainId, chainName, uri: resolvedUri },
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
logSignResult(walletMethod.type, result);
|
|
105
|
+
|
|
106
|
+
const { hash, receipt, timestamp } = await broadcastAndConfirm({
|
|
107
|
+
result,
|
|
108
|
+
chain: chainConfig.chain,
|
|
109
|
+
rpcUrl: options.rpcUrl,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const resultData = printResult(receipt, timestamp, chainConfig.chain, chainConfig.chainId, hash);
|
|
113
|
+
|
|
114
|
+
if (options.outDir) {
|
|
115
|
+
writeResultJson(options.outDir, "set-agent-uri", resultData);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface SetAgentUriResult {
|
|
120
|
+
agentId?: string;
|
|
121
|
+
newUri?: string;
|
|
122
|
+
updatedBy?: `0x${string}`;
|
|
123
|
+
chainId: number;
|
|
124
|
+
block: string;
|
|
125
|
+
timestamp: string;
|
|
126
|
+
gasPaid: string;
|
|
127
|
+
nativeCurrency: string;
|
|
128
|
+
txHash: string;
|
|
129
|
+
explorer?: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function printResult(
|
|
133
|
+
receipt: { blockNumber: bigint; gasUsed: bigint; effectiveGasPrice: bigint; logs: readonly unknown[] },
|
|
134
|
+
timestamp: bigint,
|
|
135
|
+
chain: Chain,
|
|
136
|
+
chainId: number,
|
|
137
|
+
hash: `0x${string}`,
|
|
138
|
+
): SetAgentUriResult {
|
|
139
|
+
const events = parseEventLogs({ abi: IdentityRegistryAbi, logs: receipt.logs as Log[] });
|
|
140
|
+
const uriUpdated = events.find((e) => e.eventName === "URIUpdated");
|
|
141
|
+
|
|
142
|
+
const lines: string[] = [];
|
|
143
|
+
const result: SetAgentUriResult = {
|
|
144
|
+
chainId,
|
|
145
|
+
block: receipt.blockNumber.toString(),
|
|
146
|
+
timestamp: new Date(Number(timestamp) * 1000).toUTCString(),
|
|
147
|
+
gasPaid: formatEther(receipt.gasUsed * receipt.effectiveGasPrice),
|
|
148
|
+
nativeCurrency: chain.nativeCurrency?.symbol ?? "ETH",
|
|
149
|
+
txHash: hash,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if (uriUpdated) {
|
|
153
|
+
const { agentId, newURI, updatedBy } = uriUpdated.args as {
|
|
154
|
+
agentId: bigint;
|
|
155
|
+
newURI: string;
|
|
156
|
+
updatedBy: `0x${string}`;
|
|
157
|
+
};
|
|
158
|
+
result.agentId = agentId.toString();
|
|
159
|
+
result.newUri = newURI;
|
|
160
|
+
result.updatedBy = updatedBy;
|
|
161
|
+
|
|
162
|
+
lines.push(`${label("Agent ID")}${chalk.bold(result.agentId)}`);
|
|
163
|
+
lines.push(`${label("New URI")}${truncateUri(newURI)}`);
|
|
164
|
+
lines.push(`${label("Updated By")}${updatedBy}`);
|
|
165
|
+
lines.push(`${label("Block")}${receipt.blockNumber}`);
|
|
166
|
+
} else {
|
|
167
|
+
lines.push(`${label("Block")}${receipt.blockNumber}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
lines.push(`${label("Timestamp")}${result.timestamp}`);
|
|
171
|
+
lines.push(`${label("Gas Paid")}${result.gasPaid} ${result.nativeCurrency}`);
|
|
172
|
+
lines.push(`${label("Tx Hash")}${hash}`);
|
|
173
|
+
|
|
174
|
+
const explorerUrl = getExplorerUrl(chain, hash);
|
|
175
|
+
if (explorerUrl) {
|
|
176
|
+
result.explorer = explorerUrl;
|
|
177
|
+
lines.push(`${label("Explorer")}${chalk.cyan(explorerUrl)}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log("");
|
|
181
|
+
console.log(
|
|
182
|
+
boxen(lines.join("\n"), {
|
|
183
|
+
padding: { left: 1, right: 1, top: 0, bottom: 0 },
|
|
184
|
+
borderStyle: "round",
|
|
185
|
+
borderColor: "green",
|
|
186
|
+
title: "Agent URI updated successfully",
|
|
187
|
+
titleAlignment: "left",
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { isAddress, type Chain } from "viem";
|
|
2
|
+
import { mainnet, sepolia, baseSepolia, foundry } from "viem/chains";
|
|
3
|
+
import { CHAIN_ID, getIdentityRegistryAddress } from "@aixyz/erc-8004";
|
|
4
|
+
import { select } from "@inquirer/prompts";
|
|
5
|
+
import { CliError } from "../utils";
|
|
6
|
+
|
|
7
|
+
export interface ChainConfig {
|
|
8
|
+
chain: Chain;
|
|
9
|
+
chainId: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const CHAINS: Record<string, ChainConfig> = {
|
|
13
|
+
mainnet: { chain: mainnet, chainId: CHAIN_ID.MAINNET },
|
|
14
|
+
sepolia: { chain: sepolia, chainId: CHAIN_ID.SEPOLIA },
|
|
15
|
+
"base-sepolia": { chain: baseSepolia, chainId: CHAIN_ID.BASE_SEPOLIA },
|
|
16
|
+
localhost: { chain: foundry, chainId: 31337 },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function resolveChainConfig(chainName: string): ChainConfig {
|
|
20
|
+
const config = CHAINS[chainName];
|
|
21
|
+
if (!config) {
|
|
22
|
+
throw new CliError(`Unsupported chain: ${chainName}. Supported chains: ${Object.keys(CHAINS).join(", ")}`);
|
|
23
|
+
}
|
|
24
|
+
return config;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function selectChain(): Promise<string> {
|
|
28
|
+
return select({
|
|
29
|
+
message: "Select target chain:",
|
|
30
|
+
choices: Object.keys(CHAINS).map((name) => ({ name, value: name })),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveRegistryAddress(chainName: string, chainId: number, registry?: string): `0x${string}` {
|
|
35
|
+
if (registry) {
|
|
36
|
+
if (!isAddress(registry)) {
|
|
37
|
+
throw new CliError(`Invalid registry address: ${registry}`);
|
|
38
|
+
}
|
|
39
|
+
return registry as `0x${string}`;
|
|
40
|
+
}
|
|
41
|
+
if (chainName === "localhost") {
|
|
42
|
+
throw new CliError("--registry is required for localhost (no default contract deployment)");
|
|
43
|
+
}
|
|
44
|
+
return getIdentityRegistryAddress(chainId) as `0x${string}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function validateBrowserRpcConflict(browser: boolean | undefined, rpcUrl: string | undefined): void {
|
|
48
|
+
if (browser && rpcUrl) {
|
|
49
|
+
throw new CliError("--rpc-url cannot be used with browser wallet. The browser wallet uses its own RPC endpoint.");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getExplorerUrl(chain: Chain, txHash: string): string | null {
|
|
54
|
+
const explorer = chain.blockExplorers?.default;
|
|
55
|
+
if (!explorer) return null;
|
|
56
|
+
return `${explorer.url}/tx/${txHash}`;
|
|
57
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { input } from "@inquirer/prompts";
|
|
2
|
+
|
|
3
|
+
export async function promptAgentId(): Promise<string> {
|
|
4
|
+
return input({
|
|
5
|
+
message: "Agent ID (token ID) to update:",
|
|
6
|
+
validate: (value) => {
|
|
7
|
+
const n = Number(value);
|
|
8
|
+
if (value.trim() === "" || !Number.isInteger(n) || n < 0) return "Must be a non-negative integer";
|
|
9
|
+
return true;
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function promptUri(): Promise<string> {
|
|
15
|
+
return input({
|
|
16
|
+
message: "New agent metadata URI or path to .json file (leave empty to clear):",
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
|
|
5
|
+
export function writeResultJson(outDir: string, filenamePrefix: string, result: { chainId: number }): void {
|
|
6
|
+
if (!existsSync(outDir)) {
|
|
7
|
+
mkdirSync(outDir, { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const filename = `${filenamePrefix}-${result.chainId}-${Date.now()}.json`;
|
|
11
|
+
const filePath = join(outDir, filename);
|
|
12
|
+
writeFileSync(filePath, JSON.stringify(result, null, 2) + "\n");
|
|
13
|
+
console.log(`\n${chalk.green("✓")} Result written to ${chalk.bold(filePath)}`);
|
|
14
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
2
|
+
const INTERVAL = 80;
|
|
3
|
+
|
|
4
|
+
export interface Spinner {
|
|
5
|
+
pause(): void;
|
|
6
|
+
stop(finalMessage?: string): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function startSpinner(message: string): Spinner {
|
|
10
|
+
if (!process.stderr.isTTY) {
|
|
11
|
+
process.stderr.write(`${message}\n`);
|
|
12
|
+
return { pause() {}, stop() {} };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let i = 0;
|
|
16
|
+
let timer: ReturnType<typeof setInterval> | null = setInterval(() => {
|
|
17
|
+
process.stderr.write(`\r${FRAMES[i++ % FRAMES.length]} ${message}`);
|
|
18
|
+
}, INTERVAL);
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
pause() {
|
|
22
|
+
if (timer) {
|
|
23
|
+
clearInterval(timer);
|
|
24
|
+
timer = null;
|
|
25
|
+
process.stderr.write(`\r\x1b[2K`);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
stop(finalMessage?: string) {
|
|
29
|
+
if (timer) {
|
|
30
|
+
clearInterval(timer);
|
|
31
|
+
timer = null;
|
|
32
|
+
}
|
|
33
|
+
process.stderr.write(`\r\x1b[2K`);
|
|
34
|
+
if (finalMessage) {
|
|
35
|
+
process.stderr.write(`${finalMessage}\n`);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|