@clanker-chain/clanker-cli 2026.9.7-1
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/.env.example +14 -0
- package/bin/chain-identity.mjs +212 -0
- package/bin/clanker.mjs +799 -0
- package/lib/clanker-identity-abi.mjs +500 -0
- package/lib/identity-query.mjs +364 -0
- package/lib/keys.mjs +75 -0
- package/lib/profile.mjs +232 -0
- package/lib/resolve.mjs +237 -0
- package/lib/setup-detect.mjs +136 -0
- package/lib/setup.mjs +470 -0
- package/package.json +28 -0
package/bin/clanker.mjs
ADDED
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { existsSync, writeFileSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import process from "node:process";
|
|
8
|
+
import { getAddress } from "viem";
|
|
9
|
+
import {
|
|
10
|
+
ANVIL_DEFAULT_PRIVATE_KEY,
|
|
11
|
+
harnessSnippet,
|
|
12
|
+
initProfile,
|
|
13
|
+
isLocalRpc,
|
|
14
|
+
loadConfig,
|
|
15
|
+
loadOperator,
|
|
16
|
+
} from "../lib/profile.mjs";
|
|
17
|
+
import {
|
|
18
|
+
findBotsByOperator,
|
|
19
|
+
findOperatorsByOwner,
|
|
20
|
+
labelToId,
|
|
21
|
+
pickOperatorLabel,
|
|
22
|
+
publicClientFromRpc,
|
|
23
|
+
readBot,
|
|
24
|
+
resolvePreferredOperator,
|
|
25
|
+
} from "../lib/identity-query.mjs";
|
|
26
|
+
import { resolveForRead, resolveOperatorKey, resolveReadIdentity } from "../lib/resolve.mjs";
|
|
27
|
+
import { runSetup } from "../lib/setup.mjs";
|
|
28
|
+
|
|
29
|
+
function resolveFoundryBinary(name) {
|
|
30
|
+
const home = os.homedir();
|
|
31
|
+
const candidates = [
|
|
32
|
+
join(home, ".foundry", "bin", name),
|
|
33
|
+
join("/opt/homebrew", "bin", name),
|
|
34
|
+
join("/usr/local", "bin", name),
|
|
35
|
+
];
|
|
36
|
+
for (const p of candidates) {
|
|
37
|
+
if (existsSync(p)) return p;
|
|
38
|
+
}
|
|
39
|
+
const which = spawnSync("which", [name], { encoding: "utf8" });
|
|
40
|
+
if (which.status === 0 && which.stdout?.trim()) {
|
|
41
|
+
return which.stdout.trim();
|
|
42
|
+
}
|
|
43
|
+
return name;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseChainFlags(argv, defaults) {
|
|
47
|
+
const out = { ...defaults };
|
|
48
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
49
|
+
const a = argv[i];
|
|
50
|
+
if (a === "--port" && argv[i + 1]) {
|
|
51
|
+
out.port = argv[++i];
|
|
52
|
+
} else if (a === "--host" && argv[i + 1]) {
|
|
53
|
+
out.host = argv[++i];
|
|
54
|
+
} else if (a === "--state" && argv[i + 1]) {
|
|
55
|
+
out.state = argv[++i];
|
|
56
|
+
} else if (a === "--rpc" && argv[i + 1]) {
|
|
57
|
+
out.rpc = argv[++i];
|
|
58
|
+
} else if (a === "--key" && argv[i + 1]) {
|
|
59
|
+
out.key = argv[++i];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function hasFlag(argv, name) {
|
|
66
|
+
return argv.includes(name);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function flagValue(argv, name) {
|
|
70
|
+
const i = argv.indexOf(name);
|
|
71
|
+
if (i >= 0 && argv[i + 1] && !argv[i + 1].startsWith("--")) return argv[i + 1];
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function runScript(scriptPath, args = []) {
|
|
76
|
+
const result = spawnSync(scriptPath, args, {
|
|
77
|
+
stdio: "inherit",
|
|
78
|
+
shell: false,
|
|
79
|
+
});
|
|
80
|
+
if (result.error) {
|
|
81
|
+
console.error(result.error.message);
|
|
82
|
+
process.exit(result.status ?? 1);
|
|
83
|
+
}
|
|
84
|
+
process.exit(result.status ?? 0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function findRepoRoot() {
|
|
88
|
+
const here = dirname(new URL(import.meta.url).pathname);
|
|
89
|
+
return dirname(dirname(here));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** chain up/deploy and check * need the monorepo; npm installs only ship bin/ + lib/. */
|
|
93
|
+
function requireMonorepo(repoRoot, relativePath) {
|
|
94
|
+
const target = join(repoRoot, relativePath);
|
|
95
|
+
if (!existsSync(target)) {
|
|
96
|
+
console.error(
|
|
97
|
+
`This command requires a clanker-chain git checkout (missing ${relativePath}). ` +
|
|
98
|
+
"Operator commands (init, whoami, operator, bot) work from the npm package; " +
|
|
99
|
+
"chain up/deploy and check need the monorepo.",
|
|
100
|
+
);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
return target;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function printJson(obj) {
|
|
107
|
+
console.log(JSON.stringify(obj, (_, v) => (typeof v === "bigint" ? v.toString() : v), 2));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function printHumanWhoami(data) {
|
|
111
|
+
console.log(`address: ${data.address}`);
|
|
112
|
+
console.log(`source: ${data.source}`);
|
|
113
|
+
console.log(`rpc: ${data.rpc}`);
|
|
114
|
+
console.log(`registry: ${data.registry}`);
|
|
115
|
+
if (!data.operators.length) {
|
|
116
|
+
console.log("operators: (none)");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
console.log("operators:");
|
|
120
|
+
for (const op of data.operators) {
|
|
121
|
+
const status = op.active ? "active" : "revoked";
|
|
122
|
+
console.log(` - ${op.label} [${status}] owner=${op.owner}`);
|
|
123
|
+
if (op.bots?.length) {
|
|
124
|
+
for (const b of op.bots) {
|
|
125
|
+
const bs = b.active ? "active" : "revoked";
|
|
126
|
+
console.log(` bot ${b.label} [${bs}] key=${b.botKey}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function printHumanBots(data) {
|
|
133
|
+
console.log(`operator: ${data.operator}`);
|
|
134
|
+
if (!data.bots.length) {
|
|
135
|
+
console.log("bots: (none)");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
for (const b of data.bots) {
|
|
139
|
+
const status = b.active ? "active" : "revoked";
|
|
140
|
+
console.log(` ${b.label} [${status}] key=${b.botKey} registeredAt=${b.registeredAt}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function usage() {
|
|
145
|
+
console.log(`clanker - clanker-chain helper CLI
|
|
146
|
+
|
|
147
|
+
Usage:
|
|
148
|
+
clanker setup [--preset sepolia|local] [--operator <label>] [--address 0x…] [--key-file path] [--force]
|
|
149
|
+
clanker init --preset sepolia|local [--force]
|
|
150
|
+
clanker whoami [--json] [--operator <label>] [--address 0x…]
|
|
151
|
+
clanker operator mint <label> [--json]
|
|
152
|
+
clanker operator transfer propose <label> <newOwner>
|
|
153
|
+
clanker operator transfer accept <label>
|
|
154
|
+
clanker bot mint <label> [operator] [--json]
|
|
155
|
+
clanker bots [--json] [--operator <label>] [--address 0x…]
|
|
156
|
+
clanker bot status <label> [--json]
|
|
157
|
+
clanker bot revoke <label> [--json]
|
|
158
|
+
clanker bot rotate <label> <newKeyAddress> [--json]
|
|
159
|
+
clanker init-openclaw
|
|
160
|
+
clanker chain up|deploy|mint-operator|mint-bot|rotate-bot-key|revoke-bot ...
|
|
161
|
+
clanker check mqtt <bot_id> <operator_id>
|
|
162
|
+
clanker check identity [operator_id]
|
|
163
|
+
|
|
164
|
+
Profile:
|
|
165
|
+
~/.clanker/config.json network preset (registry, RPC, broker URLs)
|
|
166
|
+
~/.clanker/operator.json label + owner + optional key pointer (never raw hex)
|
|
167
|
+
~/.clanker/keys/ bot keys (also written to ~/.openclaw/keys/)
|
|
168
|
+
|
|
169
|
+
Humans: prefer \`clanker setup\` (detects Foundry/OpenClaw hints, writes profile).
|
|
170
|
+
Key resolution (mutating commands):
|
|
171
|
+
--key > --key-file > OPERATOR_PRIVATE_KEY > profile keyFile > profile env
|
|
172
|
+
Anvil account #0 is allowed only on localhost RPC.
|
|
173
|
+
Reads (whoami/bots) can use profile owner without a signing key.
|
|
174
|
+
|
|
175
|
+
See docs/operator-cli.md.
|
|
176
|
+
`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Infer operator label from profile / whoami / flag.
|
|
181
|
+
* Preferred label uses storage read (works after OperatorTransferred).
|
|
182
|
+
*/
|
|
183
|
+
async function resolveOperatorLabel(argv, address, network) {
|
|
184
|
+
const preferred =
|
|
185
|
+
flagValue(argv, "--operator") ?? loadOperator(network.home)?.label ?? null;
|
|
186
|
+
const pub = await publicClientFromRpc(network.rpc);
|
|
187
|
+
|
|
188
|
+
if (preferred) {
|
|
189
|
+
const resolved = await resolvePreferredOperator(pub, {
|
|
190
|
+
registry: network.registry,
|
|
191
|
+
label: preferred,
|
|
192
|
+
owner: getAddress(address),
|
|
193
|
+
});
|
|
194
|
+
if (resolved.error) {
|
|
195
|
+
const err = new Error(resolved.error);
|
|
196
|
+
err.candidates = resolved.candidates;
|
|
197
|
+
throw err;
|
|
198
|
+
}
|
|
199
|
+
return { label: resolved.operator.label, operators: [resolved.operator], pub };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const operators = await findOperatorsByOwner(pub, {
|
|
203
|
+
registry: network.registry,
|
|
204
|
+
owner: getAddress(address),
|
|
205
|
+
fromBlock: network.fromBlock,
|
|
206
|
+
});
|
|
207
|
+
const pick = pickOperatorLabel({ operators, preferred: null });
|
|
208
|
+
if (pick.error) {
|
|
209
|
+
const err = new Error(pick.error);
|
|
210
|
+
err.candidates = pick.candidates;
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
return { label: pick.label, operators, pub };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function cmdWhoami(argv) {
|
|
217
|
+
let address;
|
|
218
|
+
let source;
|
|
219
|
+
let network;
|
|
220
|
+
try {
|
|
221
|
+
const resolved = resolveReadIdentity(argv);
|
|
222
|
+
address = resolved.address;
|
|
223
|
+
source = resolved.source;
|
|
224
|
+
network = resolved.network;
|
|
225
|
+
} catch (err) {
|
|
226
|
+
console.error(err.message);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const pub = await publicClientFromRpc(network.rpc);
|
|
231
|
+
const preferred =
|
|
232
|
+
flagValue(argv, "--operator") ?? loadOperator(network.home)?.label ?? null;
|
|
233
|
+
|
|
234
|
+
let selected;
|
|
235
|
+
if (preferred) {
|
|
236
|
+
const resolved = await resolvePreferredOperator(pub, {
|
|
237
|
+
registry: network.registry,
|
|
238
|
+
label: preferred,
|
|
239
|
+
owner: getAddress(address),
|
|
240
|
+
});
|
|
241
|
+
if (resolved.error) {
|
|
242
|
+
const err = new Error(resolved.error);
|
|
243
|
+
err.candidates = resolved.candidates;
|
|
244
|
+
throw err;
|
|
245
|
+
}
|
|
246
|
+
selected = [resolved.operator];
|
|
247
|
+
} else {
|
|
248
|
+
selected = await findOperatorsByOwner(pub, {
|
|
249
|
+
registry: network.registry,
|
|
250
|
+
owner: getAddress(address),
|
|
251
|
+
fromBlock: network.fromBlock,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
for (const op of selected) {
|
|
256
|
+
op.bots = await findBotsByOperator(pub, {
|
|
257
|
+
registry: network.registry,
|
|
258
|
+
operatorId: op.id,
|
|
259
|
+
fromBlock: network.fromBlock,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const data = {
|
|
264
|
+
ok: true,
|
|
265
|
+
address,
|
|
266
|
+
source,
|
|
267
|
+
rpc: network.rpc,
|
|
268
|
+
registry: network.registry,
|
|
269
|
+
operators: selected.map((o) => ({
|
|
270
|
+
label: o.label,
|
|
271
|
+
id: o.id,
|
|
272
|
+
owner: o.owner,
|
|
273
|
+
active: o.active,
|
|
274
|
+
registeredAt: o.registeredAt.toString(),
|
|
275
|
+
revokedAt: o.revokedAt.toString(),
|
|
276
|
+
bots: (o.bots ?? []).map((b) => ({
|
|
277
|
+
label: b.label,
|
|
278
|
+
botKey: b.botKey,
|
|
279
|
+
active: b.active,
|
|
280
|
+
registeredAt: b.registeredAt.toString(),
|
|
281
|
+
revokedAt: b.revokedAt.toString(),
|
|
282
|
+
})),
|
|
283
|
+
})),
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
if (hasFlag(argv, "--json")) printJson(data);
|
|
287
|
+
else printHumanWhoami(data);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function cmdBots(argv) {
|
|
291
|
+
let address;
|
|
292
|
+
let network;
|
|
293
|
+
try {
|
|
294
|
+
const resolved = resolveReadIdentity(argv);
|
|
295
|
+
address = resolved.address;
|
|
296
|
+
network = resolved.network;
|
|
297
|
+
} catch (err) {
|
|
298
|
+
console.error(err.message);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
const { label, pub } = await resolveOperatorLabel(argv, address, network);
|
|
302
|
+
const bots = await findBotsByOperator(pub, {
|
|
303
|
+
registry: network.registry,
|
|
304
|
+
operatorId: labelToId(label),
|
|
305
|
+
fromBlock: network.fromBlock,
|
|
306
|
+
});
|
|
307
|
+
const data = {
|
|
308
|
+
ok: true,
|
|
309
|
+
operator: label,
|
|
310
|
+
bots: bots.map((b) => ({
|
|
311
|
+
label: b.label,
|
|
312
|
+
botKey: b.botKey,
|
|
313
|
+
active: b.active,
|
|
314
|
+
registeredAt: b.registeredAt.toString(),
|
|
315
|
+
revokedAt: b.revokedAt.toString(),
|
|
316
|
+
mintTx: b.mintTx,
|
|
317
|
+
})),
|
|
318
|
+
};
|
|
319
|
+
if (hasFlag(argv, "--json")) printJson(data);
|
|
320
|
+
else printHumanBots(data);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function cmdBotStatus(botLabel, argv) {
|
|
324
|
+
const network = resolveForRead(argv);
|
|
325
|
+
const pub = await publicClientFromRpc(network.rpc);
|
|
326
|
+
const bot = await readBot(pub, network.registry, botLabel);
|
|
327
|
+
const data = {
|
|
328
|
+
ok: bot.registeredAt > 0n,
|
|
329
|
+
label: bot.label,
|
|
330
|
+
botKey: bot.botKey,
|
|
331
|
+
operatorId: bot.operatorId,
|
|
332
|
+
active: bot.active,
|
|
333
|
+
registeredAt: bot.registeredAt.toString(),
|
|
334
|
+
revokedAt: bot.revokedAt.toString(),
|
|
335
|
+
};
|
|
336
|
+
if (!data.ok) {
|
|
337
|
+
if (hasFlag(argv, "--json")) printJson({ ok: false, error: "bot not registered", label: botLabel });
|
|
338
|
+
else console.error(`Bot not registered: ${botLabel}`);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
if (hasFlag(argv, "--json")) printJson(data);
|
|
342
|
+
else {
|
|
343
|
+
console.log(`label: ${data.label}`);
|
|
344
|
+
console.log(`botKey: ${data.botKey}`);
|
|
345
|
+
console.log(`active: ${data.active}`);
|
|
346
|
+
console.log(`registeredAt: ${data.registeredAt}`);
|
|
347
|
+
console.log(`revokedAt: ${data.revokedAt}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function main() {
|
|
352
|
+
const [, , cmd, ...rest] = process.argv;
|
|
353
|
+
|
|
354
|
+
if (!cmd || cmd === "-h" || cmd === "--help") {
|
|
355
|
+
usage();
|
|
356
|
+
process.exit(0);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const repoRoot = findRepoRoot();
|
|
360
|
+
|
|
361
|
+
if (cmd === "setup") {
|
|
362
|
+
try {
|
|
363
|
+
await runSetup(rest);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
console.error(err.message);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (cmd === "init") {
|
|
372
|
+
const preset = flagValue(rest, "--preset") ?? rest.find((a) => !a.startsWith("--"));
|
|
373
|
+
if (!preset || preset === "init") {
|
|
374
|
+
console.error("Usage: clanker init --preset sepolia|local [--force]");
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
377
|
+
const force = hasFlag(rest, "--force");
|
|
378
|
+
const registryOverride = flagValue(rest, "--registry");
|
|
379
|
+
try {
|
|
380
|
+
const { path, config } = initProfile(preset, {
|
|
381
|
+
force,
|
|
382
|
+
registryAddress: registryOverride ?? undefined,
|
|
383
|
+
});
|
|
384
|
+
console.log(`Wrote ${path}`);
|
|
385
|
+
printJson(config);
|
|
386
|
+
} catch (err) {
|
|
387
|
+
console.error(err.message);
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (cmd === "whoami") {
|
|
394
|
+
await cmdWhoami(rest);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (cmd === "bots") {
|
|
399
|
+
await cmdBots(rest);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (cmd === "operator") {
|
|
404
|
+
const [sub, ...opArgv] = rest;
|
|
405
|
+
const {
|
|
406
|
+
chainMintOperator,
|
|
407
|
+
chainProposeOperatorTransfer,
|
|
408
|
+
chainAcceptOperatorTransfer,
|
|
409
|
+
} = await import("./chain-identity.mjs");
|
|
410
|
+
|
|
411
|
+
if (sub === "mint") {
|
|
412
|
+
const [label, ...flags] = opArgv;
|
|
413
|
+
if (!label || label.startsWith("--")) {
|
|
414
|
+
console.error("Usage: clanker operator mint <label>");
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
const result = await chainMintOperator(label, flags);
|
|
418
|
+
if (hasFlag(flags, "--json") || hasFlag(opArgv, "--json")) printJson(result);
|
|
419
|
+
else {
|
|
420
|
+
console.log(`Minted operator ${label}`);
|
|
421
|
+
console.log(`owner: ${result.owner}`);
|
|
422
|
+
console.log(`tx: ${result.tx}`);
|
|
423
|
+
console.log(`Wrote ~/.clanker/operator.json`);
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (sub === "transfer") {
|
|
429
|
+
const [action, label, newOwner, ...flags] = opArgv;
|
|
430
|
+
if (action === "propose") {
|
|
431
|
+
if (!label || !newOwner) {
|
|
432
|
+
console.error("Usage: clanker operator transfer propose <label> <newOwner>");
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
const result = await chainProposeOperatorTransfer(label, getAddress(newOwner), flags);
|
|
436
|
+
if (hasFlag(flags, "--json")) printJson(result);
|
|
437
|
+
else console.log(`Proposed transfer of ${label} → ${newOwner}\ntx: ${result.tx}`);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (action === "accept") {
|
|
441
|
+
if (!label) {
|
|
442
|
+
console.error("Usage: clanker operator transfer accept <label>");
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
const result = await chainAcceptOperatorTransfer(label, flags);
|
|
446
|
+
if (hasFlag(flags, "--json")) printJson(result);
|
|
447
|
+
else console.log(`Accepted transfer of ${label}\nowner: ${result.owner}\ntx: ${result.tx}`);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
console.error("Usage: clanker operator transfer propose|accept ...");
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
console.error("Usage: clanker operator mint|transfer ...");
|
|
455
|
+
process.exit(1);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (cmd === "bot") {
|
|
459
|
+
const [sub, ...botArgv] = rest;
|
|
460
|
+
const {
|
|
461
|
+
chainMintBot,
|
|
462
|
+
chainRotateBotKey,
|
|
463
|
+
chainRevokeBot,
|
|
464
|
+
} = await import("./chain-identity.mjs");
|
|
465
|
+
|
|
466
|
+
if (sub === "mint") {
|
|
467
|
+
const positionals = [];
|
|
468
|
+
const flags = [];
|
|
469
|
+
for (let i = 0; i < botArgv.length; i += 1) {
|
|
470
|
+
const a = botArgv[i];
|
|
471
|
+
if (a.startsWith("--")) {
|
|
472
|
+
flags.push(a);
|
|
473
|
+
if (botArgv[i + 1] && !botArgv[i + 1].startsWith("--") && a !== "--json") {
|
|
474
|
+
flags.push(botArgv[++i]);
|
|
475
|
+
}
|
|
476
|
+
} else {
|
|
477
|
+
positionals.push(a);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const [botLabel, operatorArg] = positionals;
|
|
481
|
+
if (!botLabel) {
|
|
482
|
+
console.error("Usage: clanker bot mint <label> [operator]");
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
let operatorLabel = operatorArg ?? flagValue(flags, "--operator") ?? null;
|
|
486
|
+
if (!operatorLabel) {
|
|
487
|
+
const { address, network } = resolveOperatorKey(flags);
|
|
488
|
+
const inferred = await resolveOperatorLabel(flags, address, network);
|
|
489
|
+
operatorLabel = inferred.label;
|
|
490
|
+
}
|
|
491
|
+
const result = await chainMintBot(botLabel, operatorLabel, flags);
|
|
492
|
+
if (hasFlag(flags, "--json")) printJson(result);
|
|
493
|
+
else {
|
|
494
|
+
console.log(`Minted bot ${botLabel} under ${operatorLabel}`);
|
|
495
|
+
console.log(`botKey: ${result.bot_key}`);
|
|
496
|
+
console.log(`key file: ${result.key_path}`);
|
|
497
|
+
console.log(`also: ${result.clanker_key_path}`);
|
|
498
|
+
console.log(`tx: ${result.tx}`);
|
|
499
|
+
console.log("\nchannels.mqtt stub:");
|
|
500
|
+
console.log(JSON.stringify(result.channels_mqtt, null, 2));
|
|
501
|
+
}
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (sub === "revoke") {
|
|
506
|
+
const [botLabel, ...flags] = botArgv;
|
|
507
|
+
if (!botLabel) {
|
|
508
|
+
console.error("Usage: clanker bot revoke <label>");
|
|
509
|
+
process.exit(1);
|
|
510
|
+
}
|
|
511
|
+
const result = await chainRevokeBot(botLabel, flags);
|
|
512
|
+
if (hasFlag(flags, "--json")) printJson(result);
|
|
513
|
+
else console.log(`Revoked ${botLabel}\ntx: ${result.tx}`);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (sub === "rotate") {
|
|
518
|
+
const [botLabel, newKey, ...flags] = botArgv;
|
|
519
|
+
if (!botLabel || !newKey) {
|
|
520
|
+
console.error("Usage: clanker bot rotate <label> <newKeyAddress>");
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
const result = await chainRotateBotKey(botLabel, getAddress(newKey), flags);
|
|
524
|
+
if (hasFlag(flags, "--json")) printJson(result);
|
|
525
|
+
else console.log(`Rotated ${botLabel} → ${newKey}\ntx: ${result.tx}`);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (sub === "status") {
|
|
530
|
+
const [botLabel, ...flags] = botArgv;
|
|
531
|
+
if (!botLabel) {
|
|
532
|
+
console.error("Usage: clanker bot status <label>");
|
|
533
|
+
process.exit(1);
|
|
534
|
+
}
|
|
535
|
+
await cmdBotStatus(botLabel, flags);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
console.error("Usage: clanker bot mint|revoke|rotate|status ...");
|
|
540
|
+
process.exit(1);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (cmd === "chain") {
|
|
544
|
+
const [sub, ...chainArgv] = rest;
|
|
545
|
+
if (!sub || sub === "-h" || sub === "--help") {
|
|
546
|
+
console.error(
|
|
547
|
+
"Usage: clanker chain up | deploy | mint-operator | mint-bot | rotate-bot-key | revoke-bot",
|
|
548
|
+
);
|
|
549
|
+
process.exit(1);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const {
|
|
553
|
+
chainMintOperator,
|
|
554
|
+
chainMintBot,
|
|
555
|
+
chainRotateBotKey,
|
|
556
|
+
chainRevokeBot,
|
|
557
|
+
} = await import("./chain-identity.mjs");
|
|
558
|
+
|
|
559
|
+
if (sub === "up") {
|
|
560
|
+
requireMonorepo(repoRoot, "chain");
|
|
561
|
+
const flags = parseChainFlags(chainArgv, {
|
|
562
|
+
host: "0.0.0.0",
|
|
563
|
+
port: "8545",
|
|
564
|
+
state: join(repoRoot, "chain", ".anvil-state.json"),
|
|
565
|
+
});
|
|
566
|
+
const anvil = resolveFoundryBinary("anvil");
|
|
567
|
+
if (!existsSync(anvil) && anvil === "anvil") {
|
|
568
|
+
console.error(
|
|
569
|
+
"anvil not found on PATH. Install Foundry: https://book.getfoundry.sh/getting-started/installation",
|
|
570
|
+
);
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
const stateDir = dirname(flags.state);
|
|
574
|
+
if (!existsSync(stateDir)) {
|
|
575
|
+
mkdirSync(stateDir, { recursive: true });
|
|
576
|
+
}
|
|
577
|
+
const result = spawnSync(
|
|
578
|
+
anvil,
|
|
579
|
+
["--host", flags.host, "--port", flags.port, "--state", flags.state],
|
|
580
|
+
{ stdio: "inherit", cwd: repoRoot, shell: false },
|
|
581
|
+
);
|
|
582
|
+
if (result.error) {
|
|
583
|
+
console.error(result.error.message);
|
|
584
|
+
process.exit(result.status ?? 1);
|
|
585
|
+
}
|
|
586
|
+
process.exit(result.status ?? 0);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (sub === "deploy") {
|
|
590
|
+
// Anvil-guarded key: default Anvil #0 only on local RPC.
|
|
591
|
+
const rpcFlag = flagValue(chainArgv, "--rpc") ?? "http://127.0.0.1:8545";
|
|
592
|
+
let deployKey;
|
|
593
|
+
try {
|
|
594
|
+
const resolved = resolveOperatorKey(chainArgv, {
|
|
595
|
+
rpc: rpcFlag,
|
|
596
|
+
requireRegistry: false,
|
|
597
|
+
});
|
|
598
|
+
deployKey = resolved.key;
|
|
599
|
+
} catch (err) {
|
|
600
|
+
if (isLocalRpc(rpcFlag) && !flagValue(chainArgv, "--key") && !process.env.OPERATOR_PRIVATE_KEY) {
|
|
601
|
+
deployKey = ANVIL_DEFAULT_PRIVATE_KEY;
|
|
602
|
+
} else {
|
|
603
|
+
console.error(err.message);
|
|
604
|
+
process.exit(1);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const forge = resolveFoundryBinary("forge");
|
|
608
|
+
if (!existsSync(forge) && forge === "forge") {
|
|
609
|
+
console.error(
|
|
610
|
+
"forge not found on PATH. Install Foundry: https://book.getfoundry.sh/getting-started/installation",
|
|
611
|
+
);
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
const chainDir = requireMonorepo(repoRoot, "chain");
|
|
615
|
+
const result = spawnSync(
|
|
616
|
+
forge,
|
|
617
|
+
[
|
|
618
|
+
"script",
|
|
619
|
+
"script/Deploy.s.sol:Deploy",
|
|
620
|
+
"--rpc-url",
|
|
621
|
+
rpcFlag,
|
|
622
|
+
"--private-key",
|
|
623
|
+
deployKey,
|
|
624
|
+
"--broadcast",
|
|
625
|
+
"-vvv",
|
|
626
|
+
],
|
|
627
|
+
{ cwd: chainDir, encoding: "utf8", stdio: "pipe", shell: false },
|
|
628
|
+
);
|
|
629
|
+
const combined = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
|
|
630
|
+
const match = combined.match(/ClankerIdentity deployed at:\s*(0x[a-fA-F0-9]{40})/);
|
|
631
|
+
if (match) {
|
|
632
|
+
console.log(`Deployed ClankerIdentity at ${match[1]}`);
|
|
633
|
+
} else {
|
|
634
|
+
console.log(combined);
|
|
635
|
+
if (result.status === 0) {
|
|
636
|
+
console.warn(
|
|
637
|
+
"[clanker] Could not parse deployed address from forge output; see logs above.",
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (result.error) {
|
|
642
|
+
console.error(result.error.message);
|
|
643
|
+
process.exit(result.status ?? 1);
|
|
644
|
+
}
|
|
645
|
+
process.exit(result.status ?? 0);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (sub === "mint-operator") {
|
|
649
|
+
const [label, ...flags] = chainArgv;
|
|
650
|
+
if (!label) {
|
|
651
|
+
console.error("chain mint-operator requires <label>");
|
|
652
|
+
process.exit(1);
|
|
653
|
+
}
|
|
654
|
+
const result = await chainMintOperator(label, flags);
|
|
655
|
+
printJson(result);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (sub === "mint-bot") {
|
|
660
|
+
const [botLabel, operatorLabel, ...flags] = chainArgv;
|
|
661
|
+
if (!botLabel || !operatorLabel) {
|
|
662
|
+
console.error("chain mint-bot requires <bot_label> <operator_label>");
|
|
663
|
+
process.exit(1);
|
|
664
|
+
}
|
|
665
|
+
const result = await chainMintBot(botLabel, operatorLabel, flags);
|
|
666
|
+
printJson(result);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (sub === "rotate-bot-key") {
|
|
671
|
+
const [botLabel, newKey, ...flags] = chainArgv;
|
|
672
|
+
if (!botLabel || !newKey) {
|
|
673
|
+
console.error("chain rotate-bot-key requires <bot_label> <new_key_address>");
|
|
674
|
+
process.exit(1);
|
|
675
|
+
}
|
|
676
|
+
const result = await chainRotateBotKey(botLabel, newKey, flags);
|
|
677
|
+
printJson(result);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (sub === "revoke-bot") {
|
|
682
|
+
const [botLabel, ...flags] = chainArgv;
|
|
683
|
+
if (!botLabel) {
|
|
684
|
+
console.error("chain revoke-bot requires <bot_label>");
|
|
685
|
+
process.exit(1);
|
|
686
|
+
}
|
|
687
|
+
const result = await chainRevokeBot(botLabel, flags);
|
|
688
|
+
printJson(result);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
console.error(
|
|
693
|
+
`Unknown chain subcommand: ${sub}. Use 'up', 'deploy', 'mint-operator', 'mint-bot', 'rotate-bot-key', or 'revoke-bot'.`,
|
|
694
|
+
);
|
|
695
|
+
process.exit(1);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (cmd === "check") {
|
|
699
|
+
const [sub, ...args] = rest;
|
|
700
|
+
if (sub === "mqtt") {
|
|
701
|
+
const [botId, operatorId] = args;
|
|
702
|
+
if (!botId || !operatorId) {
|
|
703
|
+
console.error("check mqtt requires <bot_id> <operator_id>");
|
|
704
|
+
process.exit(1);
|
|
705
|
+
}
|
|
706
|
+
requireMonorepo(repoRoot, "scripts");
|
|
707
|
+
const scriptPath = join(repoRoot, "scripts", "check-mqtt.sh");
|
|
708
|
+
if (!existsSync(scriptPath)) {
|
|
709
|
+
console.error(`check-mqtt.sh not found at ${scriptPath}`);
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
712
|
+
runScript(scriptPath, [botId, operatorId]);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (sub === "identity") {
|
|
716
|
+
const [operatorId] = args;
|
|
717
|
+
requireMonorepo(repoRoot, "scripts");
|
|
718
|
+
const scriptPath = join(repoRoot, "scripts", "check-identity.sh");
|
|
719
|
+
if (!existsSync(scriptPath)) {
|
|
720
|
+
console.error(`check-identity.sh not found at ${scriptPath}`);
|
|
721
|
+
process.exit(1);
|
|
722
|
+
}
|
|
723
|
+
runScript(scriptPath, operatorId ? [operatorId] : []);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
console.error("Unknown check subcommand. Use 'mqtt' or 'identity'.");
|
|
727
|
+
process.exit(1);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (cmd === "init-openclaw") {
|
|
731
|
+
const home = os.homedir();
|
|
732
|
+
const openclawDir = join(home, ".openclaw");
|
|
733
|
+
const cfgPath = join(openclawDir, "openclaw.json");
|
|
734
|
+
if (!existsSync(openclawDir)) {
|
|
735
|
+
mkdirSync(openclawDir, { recursive: true });
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (existsSync(cfgPath)) {
|
|
739
|
+
console.log(`${cfgPath} already exists.`);
|
|
740
|
+
console.log(
|
|
741
|
+
'Ensure plugins.enabled includes "mqtt" and "mqtt-tools", and channels.mqtt has botId, operatorId, brokerUrl, chainRpcUrl, registryAddress.',
|
|
742
|
+
);
|
|
743
|
+
console.log("See SETUP.md and docs/operator-cli.md.");
|
|
744
|
+
process.exit(0);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const profile = loadConfig();
|
|
748
|
+
const operator = loadOperator();
|
|
749
|
+
// No profile: local defaults only — do not mix Sepolia registry with localhost MQTT.
|
|
750
|
+
const network = profile
|
|
751
|
+
? {
|
|
752
|
+
rpc: profile.chainRpcUrl,
|
|
753
|
+
registry: profile.registryAddress,
|
|
754
|
+
brokerUrl: profile.brokerUrl,
|
|
755
|
+
mqttAuthServiceUrl: profile.mqttAuthServiceUrl,
|
|
756
|
+
}
|
|
757
|
+
: {
|
|
758
|
+
rpc: "http://127.0.0.1:8545",
|
|
759
|
+
registry: null,
|
|
760
|
+
brokerUrl: "mqtt://localhost:1883",
|
|
761
|
+
mqttAuthServiceUrl: "http://localhost:9090",
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
const mqtt = harnessSnippet({
|
|
765
|
+
botId: "openclaw.your-bot.local",
|
|
766
|
+
operatorId: operator?.label ?? "org.openclaw.your-operator",
|
|
767
|
+
network,
|
|
768
|
+
});
|
|
769
|
+
if (!mqtt.registryAddress) {
|
|
770
|
+
mqtt.registryAddress = "0x0000000000000000000000000000000000000000";
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const cfg = {
|
|
774
|
+
plugins: {
|
|
775
|
+
enabled: ["mqtt", "mqtt-tools"],
|
|
776
|
+
},
|
|
777
|
+
channels: {
|
|
778
|
+
mqtt,
|
|
779
|
+
},
|
|
780
|
+
};
|
|
781
|
+
writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
|
|
782
|
+
console.log(`Created ${cfgPath} with mqtt + mqtt-tools from ${profile ? "active" : "default"} preset.`);
|
|
783
|
+
console.log("Next: clanker bot mint <label>, then set channels.mqtt.botId / operatorId.");
|
|
784
|
+
console.log("See docs/operator-cli.md and SETUP.md.");
|
|
785
|
+
process.exit(0);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
console.error(`Unknown command: ${cmd}`);
|
|
789
|
+
usage();
|
|
790
|
+
process.exit(1);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
main().catch((err) => {
|
|
794
|
+
console.error(err?.message ?? String(err));
|
|
795
|
+
if (err?.candidates?.length) {
|
|
796
|
+
console.error(`Candidates: ${err.candidates.join(", ")}`);
|
|
797
|
+
}
|
|
798
|
+
process.exit(1);
|
|
799
|
+
});
|