arc402-cli 0.5.0 → 0.7.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/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +17 -1
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +205 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/wallet.d.ts.map +1 -1
- package/dist/commands/wallet.js +467 -23
- package/dist/commands/wallet.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -2
- package/dist/config.js.map +1 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +294 -208
- package/dist/daemon/index.js.map +1 -1
- package/dist/endpoint-notify.d.ts +7 -0
- package/dist/endpoint-notify.d.ts.map +1 -1
- package/dist/endpoint-notify.js +104 -0
- package/dist/endpoint-notify.js.map +1 -1
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +2 -0
- package/dist/program.js.map +1 -1
- package/dist/repl.d.ts.map +1 -1
- package/dist/repl.js +565 -162
- package/dist/repl.js.map +1 -1
- package/dist/ui/banner.d.ts +2 -0
- package/dist/ui/banner.d.ts.map +1 -1
- package/dist/ui/banner.js +27 -18
- package/dist/ui/banner.js.map +1 -1
- package/dist/ui/format.d.ts.map +1 -1
- package/dist/ui/format.js +2 -0
- package/dist/ui/format.js.map +1 -1
- package/dist/ui/spinner.d.ts.map +1 -1
- package/dist/ui/spinner.js +11 -0
- package/dist/ui/spinner.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/config.ts +18 -2
- package/src/commands/doctor.ts +172 -0
- package/src/commands/wallet.ts +512 -35
- package/src/config.ts +10 -1
- package/src/daemon/index.ts +234 -140
- package/src/endpoint-notify.ts +73 -0
- package/src/index.ts +15 -1
- package/src/program.ts +2 -0
- package/src/repl.ts +673 -197
- package/src/ui/banner.ts +26 -19
- package/src/ui/format.ts +1 -0
- package/src/ui/spinner.ts +10 -0
package/src/commands/wallet.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { spawnSync } from "child_process";
|
|
|
9
9
|
import { Arc402Config, getConfigPath, getUsdcAddress, loadConfig, NETWORK_DEFAULTS, saveConfig } from "../config";
|
|
10
10
|
import { getClient, requireSigner } from "../client";
|
|
11
11
|
import { getTrustTier } from "../utils/format";
|
|
12
|
-
import { ARC402_WALLET_EXECUTE_ABI, ARC402_WALLET_GUARDIAN_ABI, ARC402_WALLET_MACHINE_KEY_ABI, ARC402_WALLET_OWNER_ABI, ARC402_WALLET_PASSKEY_ABI, ARC402_WALLET_PROTOCOL_ABI, ARC402_WALLET_REGISTRY_ABI, POLICY_ENGINE_GOVERNANCE_ABI, POLICY_ENGINE_LIMITS_ABI, TRUST_REGISTRY_ABI, WALLET_FACTORY_ABI } from "../abis";
|
|
12
|
+
import { AGENT_REGISTRY_ABI, ARC402_WALLET_EXECUTE_ABI, ARC402_WALLET_GUARDIAN_ABI, ARC402_WALLET_MACHINE_KEY_ABI, ARC402_WALLET_OWNER_ABI, ARC402_WALLET_PASSKEY_ABI, ARC402_WALLET_PROTOCOL_ABI, ARC402_WALLET_REGISTRY_ABI, POLICY_ENGINE_GOVERNANCE_ABI, POLICY_ENGINE_LIMITS_ABI, TRUST_REGISTRY_ABI, WALLET_FACTORY_ABI } from "../abis";
|
|
13
13
|
import { warnIfPublicRpc } from "../config";
|
|
14
14
|
import { connectPhoneWallet, sendTransactionWithSession, requestPhoneWalletSignature } from "../walletconnect";
|
|
15
15
|
import { BundlerClient, buildSponsoredUserOp, PaymasterClient, DEFAULT_ENTRY_POINT } from "../bundler";
|
|
@@ -123,6 +123,425 @@ async function runWalletOnboardingCeremony(
|
|
|
123
123
|
console.log(" " + c.dim("arc402 wallet policy set-daily-limit --category general --amount <eth> — daily per-category cap"));
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Complete ARC-402 onboarding ceremony — matches web flow at app.arc402.xyz/onboard.
|
|
128
|
+
* Runs in a single WalletConnect session (or any sendTx provider). Idempotent.
|
|
129
|
+
*
|
|
130
|
+
* Steps:
|
|
131
|
+
* 2. Machine key — generate + authorizeMachineKey
|
|
132
|
+
* 3. Passkey — CLI shows browser URL (WebAuthn requires browser)
|
|
133
|
+
* 4. Policy — v5 protocol bypass; set hire limit + optional guardian
|
|
134
|
+
* 5. Agent — register on AgentRegistry via executeContractCall
|
|
135
|
+
* 6. Summary — branded tree
|
|
136
|
+
*/
|
|
137
|
+
async function runCompleteOnboardingCeremony(
|
|
138
|
+
walletAddress: string,
|
|
139
|
+
ownerAddress: string,
|
|
140
|
+
config: Arc402Config,
|
|
141
|
+
provider: ethers.JsonRpcProvider,
|
|
142
|
+
sendTx: (call: { to: string; data: string; value: string }, description: string) => Promise<string>,
|
|
143
|
+
): Promise<void> {
|
|
144
|
+
const policyAddress = config.policyEngineAddress ?? POLICY_ENGINE_DEFAULT;
|
|
145
|
+
const agentRegistryAddress =
|
|
146
|
+
config.agentRegistryV2Address ??
|
|
147
|
+
NETWORK_DEFAULTS[config.network]?.agentRegistryV2Address;
|
|
148
|
+
const handshakeAddress =
|
|
149
|
+
config.handshakeAddress ??
|
|
150
|
+
NETWORK_DEFAULTS[config.network]?.handshakeAddress ??
|
|
151
|
+
"0x4F5A38Bb746d7E5d49d8fd26CA6beD141Ec2DDb3";
|
|
152
|
+
|
|
153
|
+
// ── Step 2: Machine Key ────────────────────────────────────────────────────
|
|
154
|
+
console.log("\n" + c.dim("── Step 2: Machine Key ────────────────────────────────────────"));
|
|
155
|
+
|
|
156
|
+
let machineKeyAddress: string;
|
|
157
|
+
if (config.privateKey) {
|
|
158
|
+
machineKeyAddress = new ethers.Wallet(config.privateKey).address;
|
|
159
|
+
} else {
|
|
160
|
+
const mk = ethers.Wallet.createRandom();
|
|
161
|
+
machineKeyAddress = mk.address;
|
|
162
|
+
config.privateKey = mk.privateKey;
|
|
163
|
+
saveConfig(config);
|
|
164
|
+
console.log(" " + c.dim("Machine key generated: ") + c.white(machineKeyAddress));
|
|
165
|
+
console.log(" " + c.dim("Private key saved to ~/.arc402/config.json (chmod 600)"));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let mkAlreadyAuthorized = false;
|
|
169
|
+
try {
|
|
170
|
+
const mkContract = new ethers.Contract(walletAddress, ARC402_WALLET_MACHINE_KEY_ABI, provider);
|
|
171
|
+
mkAlreadyAuthorized = await mkContract.authorizedMachineKeys(machineKeyAddress) as boolean;
|
|
172
|
+
} catch { /* older wallet — will try to authorize */ }
|
|
173
|
+
|
|
174
|
+
if (mkAlreadyAuthorized) {
|
|
175
|
+
console.log(" " + c.success + c.dim(" Machine key already authorized: ") + c.white(machineKeyAddress));
|
|
176
|
+
} else {
|
|
177
|
+
console.log(" " + c.dim("Authorizing machine key: ") + c.white(machineKeyAddress));
|
|
178
|
+
const mkIface = new ethers.Interface(ARC402_WALLET_MACHINE_KEY_ABI);
|
|
179
|
+
await sendTx(
|
|
180
|
+
{ to: walletAddress, data: mkIface.encodeFunctionData("authorizeMachineKey", [machineKeyAddress]), value: "0x0" },
|
|
181
|
+
"authorizeMachineKey",
|
|
182
|
+
);
|
|
183
|
+
console.log(" " + c.success + " Machine key authorized");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── Step 3: Passkey ───────────────────────────────────────────────────────
|
|
187
|
+
console.log("\n" + c.dim("── Step 3: Passkey (Face ID / WebAuthn) ──────────────────────"));
|
|
188
|
+
|
|
189
|
+
let passkeyActive = false;
|
|
190
|
+
try {
|
|
191
|
+
const walletC = new ethers.Contract(walletAddress, ARC402_WALLET_PASSKEY_ABI, provider);
|
|
192
|
+
const auth = await walletC.ownerAuth() as [bigint, string, string];
|
|
193
|
+
passkeyActive = Number(auth[0]) === 1;
|
|
194
|
+
} catch { /* ignore */ }
|
|
195
|
+
|
|
196
|
+
if (passkeyActive) {
|
|
197
|
+
console.log(" " + c.success + c.dim(" Passkey already active"));
|
|
198
|
+
} else {
|
|
199
|
+
const passkeyUrl = `https://app.arc402.xyz/passkey-setup?wallet=${walletAddress}`;
|
|
200
|
+
console.log("\n " + c.white("Open this URL in your browser to set up Face ID:"));
|
|
201
|
+
console.log(" " + c.cyan(passkeyUrl));
|
|
202
|
+
console.log(" " + c.dim("Complete Face ID registration, then press Enter. Type 'n' + Enter to skip.\n"));
|
|
203
|
+
const passkeyAns = await prompts({
|
|
204
|
+
type: "text",
|
|
205
|
+
name: "done",
|
|
206
|
+
message: "Press Enter when done (or type 'n' to skip)",
|
|
207
|
+
initial: "",
|
|
208
|
+
});
|
|
209
|
+
if ((passkeyAns.done as string | undefined)?.toLowerCase() === "n") {
|
|
210
|
+
console.log(" " + c.warning + " Passkey skipped");
|
|
211
|
+
} else {
|
|
212
|
+
passkeyActive = true;
|
|
213
|
+
console.log(" " + c.success + " Passkey set (via browser)");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Step 4: Policy ────────────────────────────────────────────────────────
|
|
218
|
+
console.log("\n" + c.dim("── Step 4: Policy ─────────────────────────────────────────────"));
|
|
219
|
+
|
|
220
|
+
// 4a) setVelocityLimit
|
|
221
|
+
let velocityLimitEth = "0.05";
|
|
222
|
+
let velocityAlreadySet = false;
|
|
223
|
+
try {
|
|
224
|
+
const ownerC = new ethers.Contract(walletAddress, ARC402_WALLET_OWNER_ABI, provider);
|
|
225
|
+
const existing = await ownerC.velocityLimit() as bigint;
|
|
226
|
+
if (existing > 0n) {
|
|
227
|
+
velocityAlreadySet = true;
|
|
228
|
+
velocityLimitEth = ethers.formatEther(existing);
|
|
229
|
+
console.log(" " + c.success + c.dim(` Velocity limit already set: ${velocityLimitEth} ETH`));
|
|
230
|
+
}
|
|
231
|
+
} catch { /* ignore */ }
|
|
232
|
+
|
|
233
|
+
if (!velocityAlreadySet) {
|
|
234
|
+
const velAns = await prompts({
|
|
235
|
+
type: "text",
|
|
236
|
+
name: "limit",
|
|
237
|
+
message: "Velocity limit (ETH / rolling window)?",
|
|
238
|
+
initial: "0.05",
|
|
239
|
+
});
|
|
240
|
+
velocityLimitEth = (velAns.limit as string | undefined) || "0.05";
|
|
241
|
+
const velIface = new ethers.Interface(["function setVelocityLimit(uint256 limit) external"]);
|
|
242
|
+
await sendTx(
|
|
243
|
+
{ to: walletAddress, data: velIface.encodeFunctionData("setVelocityLimit", [ethers.parseEther(velocityLimitEth)]), value: "0x0" },
|
|
244
|
+
`setVelocityLimit: ${velocityLimitEth} ETH`,
|
|
245
|
+
);
|
|
246
|
+
saveConfig(config);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 4b) setGuardian (optional — address, 'g' to generate, or skip)
|
|
250
|
+
let guardianAddress: string | null = null;
|
|
251
|
+
try {
|
|
252
|
+
const guardianC = new ethers.Contract(walletAddress, ARC402_WALLET_GUARDIAN_ABI, provider);
|
|
253
|
+
const existing = await guardianC.guardian() as string;
|
|
254
|
+
if (existing && existing !== ethers.ZeroAddress) {
|
|
255
|
+
guardianAddress = existing;
|
|
256
|
+
console.log(" " + c.success + c.dim(` Guardian already set: ${existing}`));
|
|
257
|
+
}
|
|
258
|
+
} catch { /* ignore */ }
|
|
259
|
+
|
|
260
|
+
if (!guardianAddress) {
|
|
261
|
+
const guardianAns = await prompts({
|
|
262
|
+
type: "text",
|
|
263
|
+
name: "guardian",
|
|
264
|
+
message: "Guardian address? (address, 'g' to generate, Enter to skip)",
|
|
265
|
+
initial: "",
|
|
266
|
+
});
|
|
267
|
+
const guardianInput = (guardianAns.guardian as string | undefined)?.trim() ?? "";
|
|
268
|
+
if (guardianInput.toLowerCase() === "g") {
|
|
269
|
+
const generatedGuardian = ethers.Wallet.createRandom();
|
|
270
|
+
// Save guardian private key to a separate restricted file, NOT config.json
|
|
271
|
+
const guardianKeyPath = path.join(os.homedir(), ".arc402", "guardian.key");
|
|
272
|
+
fs.mkdirSync(path.dirname(guardianKeyPath), { recursive: true, mode: 0o700 });
|
|
273
|
+
fs.writeFileSync(guardianKeyPath, generatedGuardian.privateKey + "\n", { mode: 0o400 });
|
|
274
|
+
// Only save address (not private key) to config
|
|
275
|
+
config.guardianAddress = generatedGuardian.address;
|
|
276
|
+
saveConfig(config);
|
|
277
|
+
guardianAddress = generatedGuardian.address;
|
|
278
|
+
console.log("\n " + c.warning + " Guardian key saved to ~/.arc402/guardian.key — move offline for security");
|
|
279
|
+
console.log(" " + c.dim("Address: ") + c.white(generatedGuardian.address) + "\n");
|
|
280
|
+
const guardianIface = new ethers.Interface(["function setGuardian(address _guardian) external"]);
|
|
281
|
+
await sendTx(
|
|
282
|
+
{ to: walletAddress, data: guardianIface.encodeFunctionData("setGuardian", [guardianAddress]), value: "0x0" },
|
|
283
|
+
"setGuardian",
|
|
284
|
+
);
|
|
285
|
+
saveConfig(config);
|
|
286
|
+
} else if (guardianInput && ethers.isAddress(guardianInput)) {
|
|
287
|
+
guardianAddress = guardianInput;
|
|
288
|
+
config.guardianAddress = guardianInput;
|
|
289
|
+
const guardianIface = new ethers.Interface(["function setGuardian(address _guardian) external"]);
|
|
290
|
+
await sendTx(
|
|
291
|
+
{ to: walletAddress, data: guardianIface.encodeFunctionData("setGuardian", [guardianAddress]), value: "0x0" },
|
|
292
|
+
"setGuardian",
|
|
293
|
+
);
|
|
294
|
+
saveConfig(config);
|
|
295
|
+
} else if (guardianInput) {
|
|
296
|
+
console.log(" " + c.warning + " Invalid address — guardian skipped");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 4c) setCategoryLimitFor('hire')
|
|
301
|
+
let hireLimit = "0.1";
|
|
302
|
+
let hireLimitAlreadySet = false;
|
|
303
|
+
try {
|
|
304
|
+
const limitsContract = new ethers.Contract(policyAddress, POLICY_ENGINE_LIMITS_ABI, provider);
|
|
305
|
+
const existing = await limitsContract.categoryLimits(walletAddress, "hire") as bigint;
|
|
306
|
+
if (existing > 0n) {
|
|
307
|
+
hireLimitAlreadySet = true;
|
|
308
|
+
hireLimit = ethers.formatEther(existing);
|
|
309
|
+
console.log(" " + c.success + c.dim(` Hire limit already set: ${hireLimit} ETH`));
|
|
310
|
+
}
|
|
311
|
+
} catch { /* ignore */ }
|
|
312
|
+
|
|
313
|
+
if (!hireLimitAlreadySet) {
|
|
314
|
+
const limitAns = await prompts({
|
|
315
|
+
type: "text",
|
|
316
|
+
name: "limit",
|
|
317
|
+
message: "Max price per hire (ETH)?",
|
|
318
|
+
initial: "0.1",
|
|
319
|
+
});
|
|
320
|
+
hireLimit = (limitAns.limit as string | undefined) || "0.1";
|
|
321
|
+
const hireLimitWei = ethers.parseEther(hireLimit);
|
|
322
|
+
const policyIface = new ethers.Interface([
|
|
323
|
+
"function setCategoryLimitFor(address wallet, string category, uint256 limitPerTx) external",
|
|
324
|
+
]);
|
|
325
|
+
await sendTx(
|
|
326
|
+
{ to: policyAddress, data: policyIface.encodeFunctionData("setCategoryLimitFor", [walletAddress, "hire", hireLimitWei]), value: "0x0" },
|
|
327
|
+
`setCategoryLimitFor: hire → ${hireLimit} ETH`,
|
|
328
|
+
);
|
|
329
|
+
saveConfig(config);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 4d) enableContractInteraction(wallet, Handshake)
|
|
333
|
+
const contractInteractionIface = new ethers.Interface([
|
|
334
|
+
"function enableContractInteraction(address wallet, address target) external",
|
|
335
|
+
]);
|
|
336
|
+
await sendTx(
|
|
337
|
+
{ to: policyAddress, data: contractInteractionIface.encodeFunctionData("enableContractInteraction", [walletAddress, handshakeAddress]), value: "0x0" },
|
|
338
|
+
"enableContractInteraction: Handshake",
|
|
339
|
+
);
|
|
340
|
+
saveConfig(config);
|
|
341
|
+
|
|
342
|
+
console.log(" " + c.success + " Policy configured");
|
|
343
|
+
|
|
344
|
+
// ── Step 5: Agent Registration ─────────────────────────────────────────────
|
|
345
|
+
console.log("\n" + c.dim("── Step 5: Agent Registration ─────────────────────────────────"));
|
|
346
|
+
|
|
347
|
+
let agentAlreadyRegistered = false;
|
|
348
|
+
let agentName = "";
|
|
349
|
+
let agentServiceType = "";
|
|
350
|
+
let agentEndpoint = "";
|
|
351
|
+
|
|
352
|
+
if (agentRegistryAddress) {
|
|
353
|
+
try {
|
|
354
|
+
const regContract = new ethers.Contract(agentRegistryAddress, AGENT_REGISTRY_ABI, provider);
|
|
355
|
+
agentAlreadyRegistered = await regContract.isRegistered(walletAddress) as boolean;
|
|
356
|
+
if (agentAlreadyRegistered) {
|
|
357
|
+
const info = await regContract.getAgent(walletAddress) as { name: string; serviceType: string; endpoint: string };
|
|
358
|
+
agentName = info.name;
|
|
359
|
+
agentServiceType = info.serviceType;
|
|
360
|
+
agentEndpoint = info.endpoint;
|
|
361
|
+
console.log(" " + c.success + c.dim(` Agent already registered: ${agentName}`));
|
|
362
|
+
}
|
|
363
|
+
} catch { /* ignore */ }
|
|
364
|
+
|
|
365
|
+
if (!agentAlreadyRegistered) {
|
|
366
|
+
const rawHostname = os.hostname();
|
|
367
|
+
const cleanHostname = rawHostname.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
368
|
+
const answers = await prompts([
|
|
369
|
+
{ type: "text", name: "name", message: "Agent name?", initial: cleanHostname },
|
|
370
|
+
{ type: "text", name: "serviceType", message: "Service type?", initial: "intelligence" },
|
|
371
|
+
{ type: "text", name: "caps", message: "Capabilities? (comma-separated)", initial: "research" },
|
|
372
|
+
{ type: "text", name: "endpoint", message: "Endpoint?", initial: `https://${cleanHostname}.arc402.xyz` },
|
|
373
|
+
]);
|
|
374
|
+
|
|
375
|
+
agentName = (answers.name as string | undefined) || cleanHostname;
|
|
376
|
+
agentServiceType = (answers.serviceType as string | undefined) || "intelligence";
|
|
377
|
+
agentEndpoint = (answers.endpoint as string | undefined) || `https://${cleanHostname}.arc402.xyz`;
|
|
378
|
+
const capabilities: string[] = ((answers.caps as string | undefined) || "research")
|
|
379
|
+
.split(",").map((s) => s.trim()).filter(Boolean);
|
|
380
|
+
|
|
381
|
+
// 5a) enableDefiAccess (check first)
|
|
382
|
+
const peExtIface = new ethers.Interface([
|
|
383
|
+
"function enableDefiAccess(address wallet) external",
|
|
384
|
+
"function whitelistContract(address wallet, address target) external",
|
|
385
|
+
"function defiAccessEnabled(address) external view returns (bool)",
|
|
386
|
+
"function isContractWhitelisted(address wallet, address target) external view returns (bool)",
|
|
387
|
+
]);
|
|
388
|
+
const peContract = new ethers.Contract(policyAddress, peExtIface, provider);
|
|
389
|
+
|
|
390
|
+
const defiEnabled = await peContract.defiAccessEnabled(walletAddress).catch(() => false) as boolean;
|
|
391
|
+
if (!defiEnabled) {
|
|
392
|
+
await sendTx(
|
|
393
|
+
{ to: policyAddress, data: peExtIface.encodeFunctionData("enableDefiAccess", [walletAddress]), value: "0x0" },
|
|
394
|
+
"enableDefiAccess on PolicyEngine",
|
|
395
|
+
);
|
|
396
|
+
saveConfig(config);
|
|
397
|
+
} else {
|
|
398
|
+
console.log(" " + c.success + c.dim(" enableDefiAccess — already done"));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// 5b) whitelistContract for AgentRegistry (check first)
|
|
402
|
+
const whitelisted = await peContract.isContractWhitelisted(walletAddress, agentRegistryAddress).catch(() => false) as boolean;
|
|
403
|
+
if (!whitelisted) {
|
|
404
|
+
await sendTx(
|
|
405
|
+
{ to: policyAddress, data: peExtIface.encodeFunctionData("whitelistContract", [walletAddress, agentRegistryAddress]), value: "0x0" },
|
|
406
|
+
"whitelistContract: AgentRegistry on PolicyEngine",
|
|
407
|
+
);
|
|
408
|
+
saveConfig(config);
|
|
409
|
+
} else {
|
|
410
|
+
console.log(" " + c.success + c.dim(" whitelistContract(AgentRegistry) — already done"));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 5c+d) executeContractCall → register
|
|
414
|
+
const regIface = new ethers.Interface(AGENT_REGISTRY_ABI);
|
|
415
|
+
const execIface = new ethers.Interface(ARC402_WALLET_EXECUTE_ABI);
|
|
416
|
+
const regData = regIface.encodeFunctionData("register", [agentName, capabilities, agentServiceType, agentEndpoint, ""]);
|
|
417
|
+
const execData = execIface.encodeFunctionData("executeContractCall", [{
|
|
418
|
+
target: agentRegistryAddress,
|
|
419
|
+
data: regData,
|
|
420
|
+
value: 0n,
|
|
421
|
+
minReturnValue: 0n,
|
|
422
|
+
maxApprovalAmount: 0n,
|
|
423
|
+
approvalToken: ethers.ZeroAddress,
|
|
424
|
+
}]);
|
|
425
|
+
await sendTx({ to: walletAddress, data: execData, value: "0x0" }, `register agent: ${agentName}`);
|
|
426
|
+
console.log(" " + c.success + " Agent registered: " + agentName);
|
|
427
|
+
}
|
|
428
|
+
} else {
|
|
429
|
+
console.log(" " + c.warning + " AgentRegistry address not configured — skipping");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ── Step 7: Workroom Init ─────────────────────────────────────────────────
|
|
433
|
+
console.log("\n" + c.dim("── Step 7: Workroom ────────────────────────────────────────────"));
|
|
434
|
+
|
|
435
|
+
let workroomInitialized = false;
|
|
436
|
+
let dockerFound = false;
|
|
437
|
+
try {
|
|
438
|
+
const dockerCheck = spawnSync("docker", ["--version"], { encoding: "utf-8", timeout: 5000 });
|
|
439
|
+
dockerFound = dockerCheck.status === 0;
|
|
440
|
+
} catch { dockerFound = false; }
|
|
441
|
+
|
|
442
|
+
if (dockerFound) {
|
|
443
|
+
console.log(" " + c.dim("Docker found — initializing workroom..."));
|
|
444
|
+
try {
|
|
445
|
+
const initResult = spawnSync(process.execPath, [process.argv[1], "workroom", "init"], {
|
|
446
|
+
encoding: "utf-8",
|
|
447
|
+
timeout: 120000,
|
|
448
|
+
env: { ...process.env },
|
|
449
|
+
});
|
|
450
|
+
workroomInitialized = initResult.status === 0;
|
|
451
|
+
if (workroomInitialized) {
|
|
452
|
+
console.log(" " + c.success + " Workroom initialized");
|
|
453
|
+
} else {
|
|
454
|
+
console.log(" " + c.warning + " Workroom init incomplete — run: arc402 workroom init");
|
|
455
|
+
}
|
|
456
|
+
} catch {
|
|
457
|
+
console.log(" " + c.warning + " Workroom init failed — run: arc402 workroom init");
|
|
458
|
+
}
|
|
459
|
+
} else {
|
|
460
|
+
console.log(" " + c.warning + " Docker not found");
|
|
461
|
+
console.log(" Install Docker: https://docs.docker.com/get-docker/");
|
|
462
|
+
console.log(" Or run daemon in host mode: " + c.white("arc402 daemon start --host"));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ── Step 8: Daemon Start ──────────────────────────────────────────────────
|
|
466
|
+
console.log("\n" + c.dim("── Step 8: Daemon ──────────────────────────────────────────────"));
|
|
467
|
+
|
|
468
|
+
let daemonRunning = false;
|
|
469
|
+
const relayPort = 4402;
|
|
470
|
+
|
|
471
|
+
if (workroomInitialized) {
|
|
472
|
+
try {
|
|
473
|
+
const startResult = spawnSync(process.execPath, [process.argv[1], "workroom", "start"], {
|
|
474
|
+
encoding: "utf-8",
|
|
475
|
+
timeout: 30000,
|
|
476
|
+
env: { ...process.env },
|
|
477
|
+
});
|
|
478
|
+
daemonRunning = startResult.status === 0;
|
|
479
|
+
if (daemonRunning) {
|
|
480
|
+
console.log(" " + c.success + " Daemon online (port " + relayPort + ")");
|
|
481
|
+
} else {
|
|
482
|
+
console.log(" " + c.warning + " Daemon start failed — run: arc402 workroom start");
|
|
483
|
+
}
|
|
484
|
+
} catch {
|
|
485
|
+
console.log(" " + c.warning + " Daemon start failed — run: arc402 workroom start");
|
|
486
|
+
}
|
|
487
|
+
} else if (!dockerFound) {
|
|
488
|
+
try {
|
|
489
|
+
const startResult = spawnSync(process.execPath, [process.argv[1], "daemon", "start", "--host"], {
|
|
490
|
+
encoding: "utf-8",
|
|
491
|
+
timeout: 30000,
|
|
492
|
+
env: { ...process.env },
|
|
493
|
+
});
|
|
494
|
+
daemonRunning = startResult.status === 0;
|
|
495
|
+
if (daemonRunning) {
|
|
496
|
+
console.log(" " + c.success + " Daemon online — host mode (port " + relayPort + ")");
|
|
497
|
+
} else {
|
|
498
|
+
console.log(" " + c.warning + " Daemon not started — run: arc402 daemon start --host");
|
|
499
|
+
}
|
|
500
|
+
} catch {
|
|
501
|
+
console.log(" " + c.warning + " Daemon not started — run: arc402 daemon start --host");
|
|
502
|
+
}
|
|
503
|
+
} else {
|
|
504
|
+
console.log(" " + c.warning + " Daemon not started — run: arc402 workroom init first");
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ── Step 6: Summary ───────────────────────────────────────────────────────
|
|
508
|
+
const trustScore = await (async () => {
|
|
509
|
+
try {
|
|
510
|
+
const trust = new ethers.Contract(config.trustRegistryAddress, TRUST_REGISTRY_ABI, provider);
|
|
511
|
+
const s = await trust.getScore(walletAddress) as bigint;
|
|
512
|
+
return s.toString();
|
|
513
|
+
} catch { return "100"; }
|
|
514
|
+
})();
|
|
515
|
+
|
|
516
|
+
const workroomLabel = dockerFound
|
|
517
|
+
? (workroomInitialized ? (daemonRunning ? c.green("✓ Running") : c.yellow("✓ Initialized")) : c.yellow("⚠ Init needed"))
|
|
518
|
+
: c.yellow("⚠ No Docker");
|
|
519
|
+
const daemonLabel = daemonRunning
|
|
520
|
+
? c.green("✓ Online (port " + relayPort + ")")
|
|
521
|
+
: c.dim("not started");
|
|
522
|
+
const endpointLabel = agentEndpoint
|
|
523
|
+
? c.white(agentEndpoint) + c.dim(` → localhost:${relayPort}`)
|
|
524
|
+
: c.dim("—");
|
|
525
|
+
|
|
526
|
+
console.log("\n " + c.success + c.white(" Onboarding complete"));
|
|
527
|
+
renderTree([
|
|
528
|
+
{ label: "Wallet", value: c.white(walletAddress) },
|
|
529
|
+
{ label: "Owner", value: c.white(ownerAddress) },
|
|
530
|
+
{ label: "Machine", value: c.white(machineKeyAddress) + c.dim(" (authorized)") },
|
|
531
|
+
{ label: "Passkey", value: passkeyActive ? c.green("✓ set") : c.yellow("⚠ skipped") },
|
|
532
|
+
{ label: "Velocity", value: c.white(velocityLimitEth + " ETH") },
|
|
533
|
+
{ label: "Guardian", value: guardianAddress ? c.white(guardianAddress) : c.dim("none") },
|
|
534
|
+
{ label: "Hire limit", value: c.white(hireLimit + " ETH") },
|
|
535
|
+
{ label: "Agent", value: agentName ? c.white(agentName) : c.dim("not registered") },
|
|
536
|
+
{ label: "Service", value: agentServiceType ? c.white(agentServiceType) : c.dim("—") },
|
|
537
|
+
{ label: "Workroom", value: workroomLabel },
|
|
538
|
+
{ label: "Daemon", value: daemonLabel },
|
|
539
|
+
{ label: "Endpoint", value: endpointLabel },
|
|
540
|
+
{ label: "Trust", value: c.white(`${trustScore}`), last: true },
|
|
541
|
+
]);
|
|
542
|
+
console.log("\n " + c.dim("Next: fund your wallet with 0.002 ETH on Base"));
|
|
543
|
+
}
|
|
544
|
+
|
|
126
545
|
function printOpenShellHint(): void {
|
|
127
546
|
const r = spawnSync("which", ["openshell"], { encoding: "utf-8" });
|
|
128
547
|
if (r.status === 0 && r.stdout.trim()) {
|
|
@@ -270,16 +689,40 @@ export function registerWalletCommands(program: Command): void {
|
|
|
270
689
|
|
|
271
690
|
// ─── import ────────────────────────────────────────────────────────────────
|
|
272
691
|
|
|
273
|
-
wallet.command("import
|
|
274
|
-
.description("Import an existing private key")
|
|
692
|
+
wallet.command("import")
|
|
693
|
+
.description("Import an existing private key (use --key-file or stdin prompt)")
|
|
275
694
|
.option("--network <network>", "Network (base-mainnet or base-sepolia)", "base-sepolia")
|
|
276
|
-
.
|
|
695
|
+
.option("--key-file <path>", "Read private key from file instead of prompting")
|
|
696
|
+
.action(async (opts) => {
|
|
277
697
|
const network = opts.network as "base-mainnet" | "base-sepolia";
|
|
278
698
|
const defaults = NETWORK_DEFAULTS[network];
|
|
279
699
|
if (!defaults) {
|
|
280
700
|
console.error(`Unknown network: ${network}. Use base-mainnet or base-sepolia.`);
|
|
281
701
|
process.exit(1);
|
|
282
702
|
}
|
|
703
|
+
|
|
704
|
+
let privateKey: string;
|
|
705
|
+
if (opts.keyFile) {
|
|
706
|
+
try {
|
|
707
|
+
privateKey = fs.readFileSync(opts.keyFile, "utf-8").trim();
|
|
708
|
+
} catch (e) {
|
|
709
|
+
console.error(`Cannot read key file: ${e instanceof Error ? e.message : String(e)}`);
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
712
|
+
} else {
|
|
713
|
+
// Interactive prompt — hidden input avoids shell history
|
|
714
|
+
const answer = await prompts({
|
|
715
|
+
type: "password",
|
|
716
|
+
name: "key",
|
|
717
|
+
message: "Paste private key (hidden):",
|
|
718
|
+
});
|
|
719
|
+
privateKey = ((answer.key as string | undefined) ?? "").trim();
|
|
720
|
+
if (!privateKey) {
|
|
721
|
+
console.error("No private key entered.");
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
283
726
|
let imported: ethers.Wallet;
|
|
284
727
|
try {
|
|
285
728
|
imported = new ethers.Wallet(privateKey);
|
|
@@ -434,8 +877,32 @@ export function registerWalletCommands(program: Command): void {
|
|
|
434
877
|
.option("--smart-wallet", "Connect via Base Smart Wallet (Coinbase Wallet SDK) instead of WalletConnect")
|
|
435
878
|
.option("--hardware", "Hardware wallet mode: show raw wc: URI only (for Ledger Live, Trezor Suite, etc.)")
|
|
436
879
|
.option("--sponsored", "Use CDP paymaster for gas sponsorship (requires paymasterUrl + cdpKeyName + CDP_PRIVATE_KEY env)")
|
|
880
|
+
.option("--dry-run", "Simulate the deployment ceremony without sending transactions")
|
|
437
881
|
.action(async (opts) => {
|
|
438
882
|
const config = loadConfig();
|
|
883
|
+
|
|
884
|
+
if (opts.dryRun) {
|
|
885
|
+
const factoryAddr = config.walletFactoryAddress ?? NETWORK_DEFAULTS[config.network]?.walletFactoryAddress ?? "(not configured)";
|
|
886
|
+
const chainId = config.network === "base-mainnet" ? 8453 : 84532;
|
|
887
|
+
console.log();
|
|
888
|
+
console.log(" " + c.dim("── Dry run: wallet deploy ──────────────────────────────────────"));
|
|
889
|
+
console.log(" " + c.dim("Network: ") + c.white(config.network));
|
|
890
|
+
console.log(" " + c.dim("Chain ID: ") + c.white(String(chainId)));
|
|
891
|
+
console.log(" " + c.dim("RPC: ") + c.white(config.rpcUrl));
|
|
892
|
+
console.log(" " + c.dim("WalletFactory: ") + c.white(factoryAddr));
|
|
893
|
+
console.log(" " + c.dim("Signing method: ") + c.white(opts.smartWallet ? "Base Smart Wallet" : opts.hardware ? "Hardware (WC URI)" : "WalletConnect"));
|
|
894
|
+
console.log(" " + c.dim("Sponsored: ") + c.white(opts.sponsored ? "yes" : "no"));
|
|
895
|
+
console.log();
|
|
896
|
+
console.log(" " + c.dim("Steps that would run:"));
|
|
897
|
+
console.log(" 1. Connect " + (opts.smartWallet ? "Coinbase Smart Wallet" : "WalletConnect") + " session");
|
|
898
|
+
console.log(" 2. Call WalletFactory.createWallet() → deploy ARC402Wallet");
|
|
899
|
+
console.log(" 3. Save walletContractAddress to config");
|
|
900
|
+
console.log(" 4. Run onboarding ceremony (PolicyEngine, machine key, agent registration)");
|
|
901
|
+
console.log();
|
|
902
|
+
console.log(" " + c.dim("No transactions sent (--dry-run mode)."));
|
|
903
|
+
console.log();
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
439
906
|
const factoryAddress = config.walletFactoryAddress ?? NETWORK_DEFAULTS[config.network]?.walletFactoryAddress;
|
|
440
907
|
if (!factoryAddress) {
|
|
441
908
|
console.error("walletFactoryAddress not found in config or NETWORK_DEFAULTS. Add walletFactoryAddress to your config.");
|
|
@@ -655,38 +1122,30 @@ export function registerWalletCommands(program: Command): void {
|
|
|
655
1122
|
console.error("Could not find WalletCreated event in receipt. Check the transaction on-chain.");
|
|
656
1123
|
process.exit(1);
|
|
657
1124
|
}
|
|
1125
|
+
// ── Step 1 complete: save wallet + owner immediately ─────────────────
|
|
658
1126
|
config.walletContractAddress = walletAddress;
|
|
659
1127
|
config.ownerAddress = account;
|
|
660
1128
|
saveConfig(config);
|
|
661
|
-
|
|
1129
|
+
try { fs.chmodSync(getConfigPath(), 0o600); } catch { /* best-effort */ }
|
|
1130
|
+
console.log("\n " + c.success + c.white(" Wallet deployed"));
|
|
662
1131
|
renderTree([
|
|
663
1132
|
{ label: "Wallet", value: walletAddress },
|
|
664
|
-
{ label: "Owner", value: account
|
|
1133
|
+
{ label: "Owner", value: account, last: true },
|
|
665
1134
|
]);
|
|
666
1135
|
|
|
667
|
-
// ──
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
},
|
|
681
|
-
);
|
|
682
|
-
|
|
683
|
-
console.log(c.dim("Your wallet contract is ready for policy enforcement"));
|
|
684
|
-
const paymasterUrl2 = config.paymasterUrl ?? NETWORK_DEFAULTS[config.network]?.paymasterUrl;
|
|
685
|
-
const deployedBalance = await provider.getBalance(walletAddress);
|
|
686
|
-
if (paymasterUrl2 && deployedBalance < BigInt(1_000_000_000_000_000)) {
|
|
687
|
-
console.log(c.dim("Gas sponsorship active — initial setup ops are free"));
|
|
688
|
-
}
|
|
689
|
-
console.log(c.dim("\nNext: run 'arc402 wallet set-guardian' to configure the emergency guardian key."));
|
|
1136
|
+
// ── Steps 2–6: Complete onboarding ceremony (same WalletConnect session)
|
|
1137
|
+
const sendTxCeremony = async (
|
|
1138
|
+
call: { to: string; data: string; value: string },
|
|
1139
|
+
description: string,
|
|
1140
|
+
): Promise<string> => {
|
|
1141
|
+
console.log(" " + c.dim(`◈ ${description}`));
|
|
1142
|
+
const hash = await sendTransactionWithSession(client, session, account, chainId, call);
|
|
1143
|
+
console.log(" " + c.dim(" waiting for confirmation..."));
|
|
1144
|
+
await provider.waitForTransaction(hash, 1);
|
|
1145
|
+
console.log(" " + c.success + " " + c.white(description));
|
|
1146
|
+
return hash;
|
|
1147
|
+
};
|
|
1148
|
+
await runCompleteOnboardingCeremony(walletAddress, account, config, provider, sendTxCeremony);
|
|
690
1149
|
printOpenShellHint();
|
|
691
1150
|
} else {
|
|
692
1151
|
console.warn("⚠ WalletConnect not configured. Using stored private key (insecure).");
|
|
@@ -715,6 +1174,7 @@ export function registerWalletCommands(program: Command): void {
|
|
|
715
1174
|
// Generate guardian key (separate from hot key) and call setGuardian
|
|
716
1175
|
const guardianWallet = ethers.Wallet.createRandom();
|
|
717
1176
|
config.walletContractAddress = walletAddress;
|
|
1177
|
+
config.ownerAddress = address;
|
|
718
1178
|
config.guardianPrivateKey = guardianWallet.privateKey;
|
|
719
1179
|
config.guardianAddress = guardianWallet.address;
|
|
720
1180
|
saveConfig(config);
|
|
@@ -1572,16 +2032,33 @@ export function registerWalletCommands(program: Command): void {
|
|
|
1572
2032
|
console.error("walletConnectProjectId not set in config. Run `arc402 config set walletConnectProjectId <id>`.");
|
|
1573
2033
|
process.exit(1);
|
|
1574
2034
|
}
|
|
1575
|
-
const ownerAddress = config.ownerAddress;
|
|
1576
|
-
if (!ownerAddress) {
|
|
1577
|
-
console.error("ownerAddress not set in config. Run `arc402 wallet deploy` first.");
|
|
1578
|
-
process.exit(1);
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
2035
|
const policyAddress = config.policyEngineAddress ?? POLICY_ENGINE_DEFAULT;
|
|
1582
2036
|
const chainId = config.network === "base-mainnet" ? 8453 : 84532;
|
|
1583
2037
|
const provider = new ethers.JsonRpcProvider(config.rpcUrl);
|
|
1584
2038
|
|
|
2039
|
+
let ownerAddress = config.ownerAddress;
|
|
2040
|
+
if (!ownerAddress) {
|
|
2041
|
+
// Fallback 1: WalletConnect session account
|
|
2042
|
+
if (config.wcSession?.account) {
|
|
2043
|
+
ownerAddress = config.wcSession.account;
|
|
2044
|
+
console.log(c.dim(`Owner resolved from WalletConnect session: ${ownerAddress}`));
|
|
2045
|
+
} else {
|
|
2046
|
+
// Fallback 2: call owner() on the wallet contract
|
|
2047
|
+
try {
|
|
2048
|
+
const walletOwnerContract = new ethers.Contract(
|
|
2049
|
+
config.walletContractAddress!,
|
|
2050
|
+
["function owner() external view returns (address)"],
|
|
2051
|
+
provider,
|
|
2052
|
+
);
|
|
2053
|
+
ownerAddress = await walletOwnerContract.owner();
|
|
2054
|
+
console.log(c.dim(`Owner resolved from contract: ${ownerAddress}`));
|
|
2055
|
+
} catch {
|
|
2056
|
+
console.error("ownerAddress not set in config and could not be resolved from contract or WalletConnect session.");
|
|
2057
|
+
process.exit(1);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
|
|
1585
2062
|
// Encode registerWallet(wallet, owner) calldata — called on PolicyEngine
|
|
1586
2063
|
const policyInterface = new ethers.Interface([
|
|
1587
2064
|
"function registerWallet(address wallet, address owner) external",
|
package/src/config.ts
CHANGED
|
@@ -51,6 +51,10 @@ export interface Arc402Config {
|
|
|
51
51
|
const CONFIG_DIR = path.join(os.homedir(), ".arc402");
|
|
52
52
|
const CONFIG_PATH = process.env.ARC402_CONFIG || path.join(CONFIG_DIR, "config.json");
|
|
53
53
|
|
|
54
|
+
// WalletConnect project ID — get your own at cloud.walletconnect.com
|
|
55
|
+
const DEFAULT_WC_PROJECT_ID = "455e9425343b9156fce1428250c9a54a";
|
|
56
|
+
export const getWcProjectId = () => process.env.WC_PROJECT_ID ?? DEFAULT_WC_PROJECT_ID;
|
|
57
|
+
|
|
54
58
|
export const getConfigPath = () => CONFIG_PATH;
|
|
55
59
|
|
|
56
60
|
export function loadConfig(): Arc402Config {
|
|
@@ -61,7 +65,8 @@ export function loadConfig(): Arc402Config {
|
|
|
61
65
|
const autoConfig: Arc402Config = {
|
|
62
66
|
network: "base-mainnet",
|
|
63
67
|
rpcUrl: defaults.rpcUrl ?? "https://mainnet.base.org",
|
|
64
|
-
walletConnectProjectId:
|
|
68
|
+
walletConnectProjectId: getWcProjectId(),
|
|
69
|
+
ownerAddress: undefined,
|
|
65
70
|
policyEngineAddress: defaults.policyEngineAddress,
|
|
66
71
|
trustRegistryAddress: defaults.trustRegistryAddress ?? "",
|
|
67
72
|
agentRegistryAddress: d.agentRegistryV2Address ?? defaults.agentRegistryAddress,
|
|
@@ -76,6 +81,7 @@ export function loadConfig(): Arc402Config {
|
|
|
76
81
|
};
|
|
77
82
|
saveConfig(autoConfig);
|
|
78
83
|
console.log(`◈ Config auto-created at ${CONFIG_PATH} (Base Mainnet)`);
|
|
84
|
+
console.log("⚠ Base Mainnet — real funds at risk. Use arc402 config init for testnet.");
|
|
79
85
|
return autoConfig;
|
|
80
86
|
}
|
|
81
87
|
return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8")) as Arc402Config;
|
|
@@ -85,6 +91,9 @@ export function saveConfig(config: Arc402Config): void {
|
|
|
85
91
|
const configDir = path.dirname(CONFIG_PATH);
|
|
86
92
|
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
87
93
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
94
|
+
if (config.privateKey) {
|
|
95
|
+
console.warn("⚠ Private key stored in plaintext at ~/.arc402/config.json");
|
|
96
|
+
}
|
|
88
97
|
}
|
|
89
98
|
|
|
90
99
|
export const configExists = () => fs.existsSync(CONFIG_PATH);
|