@clanker-chain/clanker-cli 2026.9.7 → 2026.9.8-2
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/bin/clanker.mjs +258 -92
- package/lib/doctor.mjs +226 -0
- package/lib/foundry.mjs +114 -0
- package/lib/openclaw-wire.mjs +145 -0
- package/lib/operator-key.mjs +50 -0
- package/lib/profile.mjs +17 -9
- package/lib/resolve.mjs +57 -1
- package/lib/setup-detect.mjs +224 -0
- package/lib/setup.mjs +741 -0
- package/lib/ui.mjs +104 -0
- package/package.json +3 -1
package/lib/doctor.mjs
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `clanker doctor` — local readiness checks (mise-doctor habit).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getAddress } from "viem";
|
|
6
|
+
import {
|
|
7
|
+
ANVIL_DEFAULT_ADDRESS,
|
|
8
|
+
isLocalRpc,
|
|
9
|
+
loadConfig,
|
|
10
|
+
loadOperator,
|
|
11
|
+
clankerHome,
|
|
12
|
+
} from "./profile.mjs";
|
|
13
|
+
import { detectSetupHints, formatSetupDetectTable } from "./setup-detect.mjs";
|
|
14
|
+
import { c, nextHint } from "./ui.mjs";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {{ home?: string, env?: NodeJS.ProcessEnv, openclawDir?: string, castBin?: string, spawn?: Function, fetchImpl?: typeof fetch }} [opts]
|
|
18
|
+
*/
|
|
19
|
+
export async function runDoctorChecks(opts = {}) {
|
|
20
|
+
const env = opts.env ?? process.env;
|
|
21
|
+
const home = opts.home ?? clankerHome(env);
|
|
22
|
+
const hints = detectSetupHints({
|
|
23
|
+
home,
|
|
24
|
+
env,
|
|
25
|
+
openclawDir: opts.openclawDir,
|
|
26
|
+
castBin: opts.castBin,
|
|
27
|
+
spawn: opts.spawn,
|
|
28
|
+
});
|
|
29
|
+
const config = hints.config ?? loadConfig(home);
|
|
30
|
+
const operator = hints.operator ?? loadOperator(home);
|
|
31
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
32
|
+
|
|
33
|
+
/** @type {{ id: string, ok: boolean, level: 'pass'|'warn'|'fail', message: string }[]} */
|
|
34
|
+
const checks = [];
|
|
35
|
+
|
|
36
|
+
checks.push({
|
|
37
|
+
id: "config",
|
|
38
|
+
ok: Boolean(config),
|
|
39
|
+
level: config ? "pass" : "fail",
|
|
40
|
+
message: config
|
|
41
|
+
? `config.json present (preset=${config.preset})`
|
|
42
|
+
: "config.json missing — run clanker setup",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const registry = config?.registryAddress;
|
|
46
|
+
const rpc = config?.chainRpcUrl ?? "";
|
|
47
|
+
checks.push({
|
|
48
|
+
id: "registry",
|
|
49
|
+
ok: Boolean(registry && /^0x[0-9a-fA-F]{40}$/.test(registry)),
|
|
50
|
+
level: registry && /^0x[0-9a-fA-F]{40}$/.test(registry) ? "pass" : "fail",
|
|
51
|
+
message:
|
|
52
|
+
registry && /^0x[0-9a-fA-F]{40}$/.test(registry)
|
|
53
|
+
? `registry ${registry}`
|
|
54
|
+
: "registry missing — run clanker setup --preset sepolia (or set after local deploy)",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const hasOwner =
|
|
58
|
+
Boolean(operator?.owner) && /^0x[0-9a-fA-F]{40}$/.test(operator.owner);
|
|
59
|
+
const hasLabel = Boolean(operator?.label);
|
|
60
|
+
checks.push({
|
|
61
|
+
id: "operator",
|
|
62
|
+
ok: hasOwner && hasLabel,
|
|
63
|
+
level: hasOwner && hasLabel ? "pass" : "fail",
|
|
64
|
+
message:
|
|
65
|
+
hasOwner && hasLabel
|
|
66
|
+
? `operator.json ${operator.label} · ${operator.owner}`
|
|
67
|
+
: "operator.json incomplete — run clanker setup",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (hasOwner && rpc && !isLocalRpc(rpc)) {
|
|
71
|
+
const anvil =
|
|
72
|
+
getAddress(operator.owner).toLowerCase() ===
|
|
73
|
+
ANVIL_DEFAULT_ADDRESS.toLowerCase();
|
|
74
|
+
checks.push({
|
|
75
|
+
id: "anvil_public",
|
|
76
|
+
ok: !anvil,
|
|
77
|
+
level: anvil ? "fail" : "pass",
|
|
78
|
+
message: anvil
|
|
79
|
+
? "owner is Anvil #0 on a public RPC — refuse for mutate; fix owner address"
|
|
80
|
+
: "owner is not Anvil #0",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const hasKey =
|
|
85
|
+
Boolean(operator?.key?.type === "keyFile" && operator.key.value) ||
|
|
86
|
+
Boolean(operator?.key?.type === "env" && operator.key.value) ||
|
|
87
|
+
Boolean(env.OPERATOR_PRIVATE_KEY);
|
|
88
|
+
checks.push({
|
|
89
|
+
id: "signing",
|
|
90
|
+
ok: true,
|
|
91
|
+
level: hasKey ? "pass" : "warn",
|
|
92
|
+
message: hasKey
|
|
93
|
+
? "signing key pointer available (mint/revoke OK)"
|
|
94
|
+
: "read-only profile — whoami/bots OK; mint needs --key-file or OPERATOR_PRIVATE_KEY",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
checks.push({
|
|
98
|
+
id: "foundry",
|
|
99
|
+
ok: true,
|
|
100
|
+
level: hints.foundryAvailable ? "pass" : "warn",
|
|
101
|
+
message: hints.foundryAvailable
|
|
102
|
+
? `Foundry cast OK (${hints.foundryAccounts.length} account(s))`
|
|
103
|
+
: "Foundry cast not on PATH (optional)",
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const authUrl = config?.mqttAuthServiceUrl;
|
|
107
|
+
if (authUrl && typeof fetchImpl === "function") {
|
|
108
|
+
const healthUrl = `${String(authUrl).replace(/\/$/, "")}/health`;
|
|
109
|
+
try {
|
|
110
|
+
const ac = new AbortController();
|
|
111
|
+
const t = setTimeout(() => ac.abort(), 5000);
|
|
112
|
+
const res = await fetchImpl(healthUrl, { signal: ac.signal });
|
|
113
|
+
clearTimeout(t);
|
|
114
|
+
if (res.ok) {
|
|
115
|
+
checks.push({
|
|
116
|
+
id: "mqtt_auth_health",
|
|
117
|
+
ok: true,
|
|
118
|
+
level: "pass",
|
|
119
|
+
message: `mqtt-auth health OK (${healthUrl})`,
|
|
120
|
+
});
|
|
121
|
+
} else {
|
|
122
|
+
checks.push({
|
|
123
|
+
id: "mqtt_auth_health",
|
|
124
|
+
ok: false,
|
|
125
|
+
level: "fail",
|
|
126
|
+
message: `mqtt-auth health HTTP ${res.status} (${healthUrl})`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
checks.push({
|
|
131
|
+
id: "mqtt_auth_health",
|
|
132
|
+
ok: false,
|
|
133
|
+
level: "warn",
|
|
134
|
+
message: `mqtt-auth health unreachable: ${err.message ?? err}`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
} else if (config) {
|
|
138
|
+
checks.push({
|
|
139
|
+
id: "mqtt_auth_health",
|
|
140
|
+
ok: true,
|
|
141
|
+
level: "warn",
|
|
142
|
+
message: "mqttAuthServiceUrl not set — skip hub health",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const readyWhoami = checks
|
|
147
|
+
.filter((ch) => ch.id === "config" || ch.id === "registry" || ch.id === "operator")
|
|
148
|
+
.every((ch) => ch.ok);
|
|
149
|
+
const readyMint = readyWhoami && hasKey &&
|
|
150
|
+
!checks.some((ch) => ch.id === "anvil_public" && !ch.ok);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
home,
|
|
154
|
+
hints,
|
|
155
|
+
checks,
|
|
156
|
+
readyWhoami,
|
|
157
|
+
readyMint,
|
|
158
|
+
ok: readyWhoami,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {string[]} argv
|
|
164
|
+
* @param {{ home?: string, env?: NodeJS.ProcessEnv }} [opts]
|
|
165
|
+
* @returns {Promise<{ exitCode: number, report: object }>}
|
|
166
|
+
*/
|
|
167
|
+
export async function runDoctor(argv = [], opts = {}) {
|
|
168
|
+
const json = argv.includes("--json");
|
|
169
|
+
const report = await runDoctorChecks(opts);
|
|
170
|
+
|
|
171
|
+
if (json) {
|
|
172
|
+
console.log(
|
|
173
|
+
JSON.stringify(
|
|
174
|
+
{
|
|
175
|
+
ok: report.ok,
|
|
176
|
+
readyWhoami: report.readyWhoami,
|
|
177
|
+
readyMint: report.readyMint,
|
|
178
|
+
home: report.home,
|
|
179
|
+
checks: report.checks,
|
|
180
|
+
},
|
|
181
|
+
null,
|
|
182
|
+
2,
|
|
183
|
+
),
|
|
184
|
+
);
|
|
185
|
+
return { exitCode: report.ok ? 0 : 1, report };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log(c.bold("clanker doctor"));
|
|
189
|
+
console.log("");
|
|
190
|
+
console.log(formatSetupDetectTable(report.hints));
|
|
191
|
+
console.log("");
|
|
192
|
+
console.log(c.bold("Checks"));
|
|
193
|
+
for (const ch of report.checks) {
|
|
194
|
+
const mark =
|
|
195
|
+
ch.level === "pass"
|
|
196
|
+
? c.green("pass")
|
|
197
|
+
: ch.level === "warn"
|
|
198
|
+
? c.yellow("warn")
|
|
199
|
+
: c.red("fail");
|
|
200
|
+
console.log(` [${mark}] ${ch.message}`);
|
|
201
|
+
}
|
|
202
|
+
console.log("");
|
|
203
|
+
if (report.readyWhoami) {
|
|
204
|
+
console.log(c.green("Ready for: clanker whoami"));
|
|
205
|
+
} else {
|
|
206
|
+
console.log(c.red("Not ready for whoami"));
|
|
207
|
+
}
|
|
208
|
+
if (report.readyMint) {
|
|
209
|
+
console.log(c.green("Ready for: clanker operator mint / bot mint"));
|
|
210
|
+
} else {
|
|
211
|
+
console.log(c.dim("Mint/revoke: need signing key (and non-Anvil owner on public RPC)"));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!report.readyWhoami) {
|
|
215
|
+
nextHint(["clanker setup"]);
|
|
216
|
+
} else if (!report.readyMint) {
|
|
217
|
+
nextHint([
|
|
218
|
+
"clanker whoami",
|
|
219
|
+
"clanker setup --key-file ~/.clanker/op.key --force # to enable mint",
|
|
220
|
+
]);
|
|
221
|
+
} else {
|
|
222
|
+
nextHint(["clanker whoami", "clanker bots"]);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return { exitCode: report.ok ? 0 : 1, report };
|
|
226
|
+
}
|
package/lib/foundry.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Foundry cast helpers for setup (address resolve + key export).
|
|
3
|
+
* Never logs private keys.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { getAddress } from "viem";
|
|
10
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
11
|
+
import { normalizePrivateKey } from "./resolve.mjs";
|
|
12
|
+
import { parseCastWalletList } from "./setup-detect.mjs";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {{ castBin?: string, spawn?: typeof spawnSync }} [opts]
|
|
16
|
+
*/
|
|
17
|
+
export function listFoundryAccounts(opts = {}) {
|
|
18
|
+
const spawn = opts.spawn ?? spawnSync;
|
|
19
|
+
const castBin = opts.castBin ?? "cast";
|
|
20
|
+
const result = spawn(castBin, ["wallet", "list"], {
|
|
21
|
+
encoding: "utf8",
|
|
22
|
+
shell: false,
|
|
23
|
+
});
|
|
24
|
+
if (result.error || result.status !== 0) {
|
|
25
|
+
return { available: false, accounts: [] };
|
|
26
|
+
}
|
|
27
|
+
return { available: true, accounts: parseCastWalletList(result.stdout ?? "") };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve 0x address for a Foundry account name.
|
|
32
|
+
* Uses inherit stdio so unlock prompts work when interactive.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} account
|
|
35
|
+
* @param {{ castBin?: string, spawn?: typeof spawnSync, inheritStdio?: boolean }} [opts]
|
|
36
|
+
* @returns {string} checksummed address
|
|
37
|
+
*/
|
|
38
|
+
export function resolveFoundryAddress(account, opts = {}) {
|
|
39
|
+
const spawn = opts.spawn ?? spawnSync;
|
|
40
|
+
const castBin = opts.castBin ?? "cast";
|
|
41
|
+
const inherit = opts.inheritStdio !== false;
|
|
42
|
+
const result = spawn(castBin, ["wallet", "address", account], {
|
|
43
|
+
encoding: "utf8",
|
|
44
|
+
shell: false,
|
|
45
|
+
stdio: inherit ? ["inherit", "pipe", "inherit"] : "pipe",
|
|
46
|
+
});
|
|
47
|
+
if (result.error) {
|
|
48
|
+
throw new Error(`cast failed: ${result.error.message}`);
|
|
49
|
+
}
|
|
50
|
+
if (result.status !== 0) {
|
|
51
|
+
const err = (result.stderr || result.stdout || "").trim();
|
|
52
|
+
throw new Error(
|
|
53
|
+
err || `cast wallet address ${account} failed (status ${result.status})`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const line = String(result.stdout ?? "")
|
|
57
|
+
.split(/\r?\n/)
|
|
58
|
+
.map((l) => l.trim())
|
|
59
|
+
.find((l) => /^0x[0-9a-fA-F]{40}$/.test(l));
|
|
60
|
+
if (!line) {
|
|
61
|
+
throw new Error(`Could not parse address from cast wallet address ${account}`);
|
|
62
|
+
}
|
|
63
|
+
return getAddress(line);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Export Foundry account private key to destPath (mode 0o600).
|
|
68
|
+
* Does not print the key. Unlock prompts via inherit stdio when interactive.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} account
|
|
71
|
+
* @param {string} destPath
|
|
72
|
+
* @param {{ castBin?: string, spawn?: typeof spawnSync, inheritStdio?: boolean }} [opts]
|
|
73
|
+
* @returns {{ path: string, address: string }}
|
|
74
|
+
*/
|
|
75
|
+
export function exportFoundryKey(account, destPath, opts = {}) {
|
|
76
|
+
const spawn = opts.spawn ?? spawnSync;
|
|
77
|
+
const castBin = opts.castBin ?? "cast";
|
|
78
|
+
const inherit = opts.inheritStdio !== false;
|
|
79
|
+
const result = spawn(castBin, ["wallet", "private-key", account], {
|
|
80
|
+
encoding: "utf8",
|
|
81
|
+
shell: false,
|
|
82
|
+
stdio: inherit ? ["inherit", "pipe", "inherit"] : "pipe",
|
|
83
|
+
});
|
|
84
|
+
if (result.error) {
|
|
85
|
+
throw new Error(`cast failed: ${result.error.message}`);
|
|
86
|
+
}
|
|
87
|
+
if (result.status !== 0) {
|
|
88
|
+
const err = (result.stderr || "").trim();
|
|
89
|
+
throw new Error(
|
|
90
|
+
err || `cast wallet private-key ${account} failed (status ${result.status})`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const raw = String(result.stdout ?? "")
|
|
94
|
+
.split(/\r?\n/)
|
|
95
|
+
.map((l) => l.trim())
|
|
96
|
+
.find((l) => l.length > 0);
|
|
97
|
+
if (!raw) {
|
|
98
|
+
throw new Error("cast returned empty private key");
|
|
99
|
+
}
|
|
100
|
+
const key = normalizePrivateKey(raw);
|
|
101
|
+
const address = privateKeyToAccount(key).address;
|
|
102
|
+
|
|
103
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
104
|
+
writeFileSync(destPath, `${key}\n`, { mode: 0o600 });
|
|
105
|
+
return { path: destPath, address };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Default operator key path under clanker home.
|
|
110
|
+
* @param {string} home
|
|
111
|
+
*/
|
|
112
|
+
export function defaultOperatorKeyPath(home) {
|
|
113
|
+
return `${home.replace(/\/$/, "")}/op.key`;
|
|
114
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge / create ~/.openclaw/openclaw.json channels.mqtt from clanker profile.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { harnessSnippet } from "./profile.mjs";
|
|
9
|
+
|
|
10
|
+
/** Current published OpenClaw plugin pins for closed-beta invite. */
|
|
11
|
+
export const OPENCLAW_PLUGIN_PIN = "2026.7.29";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} [openclawHome]
|
|
15
|
+
*/
|
|
16
|
+
export function openclawConfigPath(openclawHome = join(homedir(), ".openclaw")) {
|
|
17
|
+
return join(openclawHome, "openclaw.json");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Ensure mqtt + mqtt-tools are in plugins.enabled.
|
|
22
|
+
* @param {object} cfg
|
|
23
|
+
*/
|
|
24
|
+
export function ensureMqttPlugins(cfg) {
|
|
25
|
+
const out = { ...cfg };
|
|
26
|
+
const plugins = { ...(out.plugins ?? {}) };
|
|
27
|
+
const enabled = Array.isArray(plugins.enabled) ? [...plugins.enabled] : [];
|
|
28
|
+
for (const id of ["mqtt", "mqtt-tools"]) {
|
|
29
|
+
if (!enabled.includes(id)) enabled.push(id);
|
|
30
|
+
}
|
|
31
|
+
plugins.enabled = enabled;
|
|
32
|
+
out.plugins = plugins;
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Wire channels.mqtt into openclaw.json.
|
|
38
|
+
*
|
|
39
|
+
* @param {{
|
|
40
|
+
* botId: string,
|
|
41
|
+
* operatorId: string,
|
|
42
|
+
* network: { rpc: string, registry: string|null, brokerUrl?: string, mqttAuthServiceUrl?: string },
|
|
43
|
+
* keyPath?: string|null,
|
|
44
|
+
* openclawHome?: string,
|
|
45
|
+
* }} opts
|
|
46
|
+
*/
|
|
47
|
+
export function wireOpenClawMqtt(opts) {
|
|
48
|
+
const openclawHome = opts.openclawHome ?? join(homedir(), ".openclaw");
|
|
49
|
+
const cfgPath = openclawConfigPath(openclawHome);
|
|
50
|
+
mkdirSync(openclawHome, { recursive: true });
|
|
51
|
+
|
|
52
|
+
const mqtt = harnessSnippet({
|
|
53
|
+
botId: opts.botId,
|
|
54
|
+
operatorId: opts.operatorId,
|
|
55
|
+
network: opts.network,
|
|
56
|
+
});
|
|
57
|
+
if (!mqtt.registryAddress) {
|
|
58
|
+
mqtt.registryAddress = "0x0000000000000000000000000000000000000000";
|
|
59
|
+
}
|
|
60
|
+
if (opts.keyPath) {
|
|
61
|
+
mqtt.privateKeyFile = opts.keyPath;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let created = false;
|
|
65
|
+
let cfg;
|
|
66
|
+
if (!existsSync(cfgPath)) {
|
|
67
|
+
created = true;
|
|
68
|
+
cfg = {
|
|
69
|
+
plugins: { enabled: ["mqtt", "mqtt-tools"] },
|
|
70
|
+
channels: { mqtt },
|
|
71
|
+
};
|
|
72
|
+
} else {
|
|
73
|
+
cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
|
|
74
|
+
cfg = ensureMqttPlugins(cfg);
|
|
75
|
+
const channels = { ...(cfg.channels ?? {}) };
|
|
76
|
+
const prev = channels.mqtt && typeof channels.mqtt === "object" ? channels.mqtt : {};
|
|
77
|
+
channels.mqtt = { ...prev, ...mqtt };
|
|
78
|
+
cfg.channels = channels;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
writeFileSync(cfgPath, `${JSON.stringify(cfg, null, 2)}\n`, "utf8");
|
|
82
|
+
return {
|
|
83
|
+
created,
|
|
84
|
+
path: cfgPath,
|
|
85
|
+
channelsMqtt: cfg.channels.mqtt,
|
|
86
|
+
pluginPin: OPENCLAW_PLUGIN_PIN,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Human checklist lines after wiring.
|
|
92
|
+
* @param {{ botId: string, channelsMqtt: object, pluginPin: string }} opts
|
|
93
|
+
*/
|
|
94
|
+
export function hubConnectChecklist(opts) {
|
|
95
|
+
const pin = opts.pluginPin ?? OPENCLAW_PLUGIN_PIN;
|
|
96
|
+
return [
|
|
97
|
+
"Your bot login is already wired — install plugins and restart OpenClaw.",
|
|
98
|
+
`openclaw plugins install @clanker-chain/mqtt-channel-plugin@${pin}`,
|
|
99
|
+
`openclaw plugins install @clanker-chain/mqtt-tools@${pin}`,
|
|
100
|
+
"Enable plugin ids mqtt + mqtt-tools (already set in openclaw.json if we wired it)",
|
|
101
|
+
`CONNECT broker: ${opts.channelsMqtt?.brokerUrl ?? "mqtts://mqtt.clanker-chain.com:8883"}`,
|
|
102
|
+
`Ask hub operator to allow bot_id "${opts.botId}" in france dmPolicy / allowFrom`,
|
|
103
|
+
"DM openclaw.france.prod-1 to smoke the mesh",
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Plain-language card: what the bot uses vs operator mint key.
|
|
109
|
+
* @param {{
|
|
110
|
+
* botId: string,
|
|
111
|
+
* operatorId: string,
|
|
112
|
+
* keyPath: string,
|
|
113
|
+
* openclawPath: string,
|
|
114
|
+
* created?: boolean,
|
|
115
|
+
* }} opts
|
|
116
|
+
* @returns {string[]}
|
|
117
|
+
*/
|
|
118
|
+
export function botIdentityCard(opts) {
|
|
119
|
+
const openclawState = opts.created
|
|
120
|
+
? `${opts.openclawPath} (channels.mqtt created)`
|
|
121
|
+
: `${opts.openclawPath} (channels.mqtt updated)`;
|
|
122
|
+
return [
|
|
123
|
+
"Bot identity (what OpenClaw uses to CONNECT):",
|
|
124
|
+
`bot id: ${opts.botId}`,
|
|
125
|
+
`operator: ${opts.operatorId}`,
|
|
126
|
+
`bot key: ${opts.keyPath}`,
|
|
127
|
+
`openclaw: ${openclawState}`,
|
|
128
|
+
"Do not give the bot ~/.clanker/op.key — that key is only for mint/transfer.",
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Structured bot_identity for --json mint output.
|
|
134
|
+
* @param {{ botId: string, keyPath: string, openclawConfigPath: string }} opts
|
|
135
|
+
*/
|
|
136
|
+
export function botIdentityJson(opts) {
|
|
137
|
+
return {
|
|
138
|
+
botId: opts.botId,
|
|
139
|
+
keyPath: opts.keyPath,
|
|
140
|
+
openclawConfigPath: opts.openclawConfigPath,
|
|
141
|
+
warnOperatorKey:
|
|
142
|
+
"Do not give the bot ~/.clanker/op.key — that key is only for mint/transfer.",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consumer-path operator key helpers (generate local op.key).
|
|
3
|
+
* Never logs private keys.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
|
9
|
+
import { defaultOperatorKeyPath } from "./foundry.mjs";
|
|
10
|
+
|
|
11
|
+
export { defaultOperatorKeyPath };
|
|
12
|
+
|
|
13
|
+
/** Coinbase Developer Platform faucet UI (Base Sepolia). */
|
|
14
|
+
export const BASE_SEPOLIA_FAUCET_URL =
|
|
15
|
+
"https://portal.cdp.coinbase.com/products/faucet";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate a new secp256k1 key and write it to destPath (mode 0o600).
|
|
19
|
+
* Does not print the key.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} destPath
|
|
22
|
+
* @param {{ force?: boolean }} [opts]
|
|
23
|
+
* @returns {{ path: string, address: string }}
|
|
24
|
+
*/
|
|
25
|
+
export function generateOperatorKeyFile(destPath, opts = {}) {
|
|
26
|
+
if (existsSync(destPath) && !opts.force) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Key file already exists: ${destPath} (pass --force to overwrite)`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const key = generatePrivateKey();
|
|
32
|
+
const address = privateKeyToAccount(key).address;
|
|
33
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
34
|
+
writeFileSync(destPath, `${key}\n`, { mode: 0o600 });
|
|
35
|
+
return { path: destPath, address };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Plain-language next steps after creating an operator key on Sepolia.
|
|
40
|
+
* @param {{ address: string, label?: string }} opts
|
|
41
|
+
*/
|
|
42
|
+
export function consumerFundHints(opts) {
|
|
43
|
+
const mintLabel = opts.label ? ` ${opts.label}` : "";
|
|
44
|
+
return [
|
|
45
|
+
`Your operator address is ${opts.address} — fund it with Base Sepolia ETH: ${BASE_SEPOLIA_FAUCET_URL}`,
|
|
46
|
+
"clanker doctor",
|
|
47
|
+
`clanker operator mint${mintLabel} --yes`,
|
|
48
|
+
"clanker bot mint <bot_label> --yes",
|
|
49
|
+
];
|
|
50
|
+
}
|
package/lib/profile.mjs
CHANGED
|
@@ -17,11 +17,14 @@ export const ANVIL_DEFAULT_ADDRESS = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
|
|
|
17
17
|
export const SEPOLIA_REGISTRY = "0xD650467f9D7A20f37E55ec23Ca1c711598f97958";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Safe floor for Sepolia getLogs (before registry deploy).
|
|
21
|
-
*
|
|
20
|
+
* Safe floor for Sepolia getLogs (before registry deploy). Use for full historical
|
|
21
|
+
* scans via --from-block; default preset uses the faster public-RPC floor below.
|
|
22
22
|
*/
|
|
23
23
|
export const SEPOLIA_FROM_BLOCK = 35_000_000n;
|
|
24
24
|
|
|
25
|
+
/** Default Sepolia fromBlock for public RPC (faster whoami/bots scans). */
|
|
26
|
+
export const SEPOLIA_FAST_FROM_BLOCK = 46_000_000n;
|
|
27
|
+
|
|
25
28
|
export const PRESETS = {
|
|
26
29
|
local: {
|
|
27
30
|
preset: "local",
|
|
@@ -37,7 +40,7 @@ export const PRESETS = {
|
|
|
37
40
|
chainRpcUrl: "https://sepolia.base.org",
|
|
38
41
|
brokerUrl: "mqtts://mqtt.clanker-chain.com:8883",
|
|
39
42
|
mqttAuthServiceUrl: "https://mqtt-auth.clanker-chain.com",
|
|
40
|
-
fromBlock:
|
|
43
|
+
fromBlock: SEPOLIA_FAST_FROM_BLOCK,
|
|
41
44
|
},
|
|
42
45
|
};
|
|
43
46
|
|
|
@@ -63,7 +66,7 @@ export function openclawKeysDir() {
|
|
|
63
66
|
|
|
64
67
|
/**
|
|
65
68
|
* @param {string} presetName
|
|
66
|
-
* @param {{ force?: boolean, registryAddress?: string|null, home?: string }} [opts]
|
|
69
|
+
* @param {{ force?: boolean, registryAddress?: string|null, home?: string, fromBlock?: bigint|string|number }} [opts]
|
|
67
70
|
*/
|
|
68
71
|
export function initProfile(presetName, opts = {}) {
|
|
69
72
|
const name = String(presetName || "").toLowerCase();
|
|
@@ -79,13 +82,15 @@ export function initProfile(presetName, opts = {}) {
|
|
|
79
82
|
}
|
|
80
83
|
mkdirSync(home, { recursive: true });
|
|
81
84
|
const preset = PRESETS[name];
|
|
85
|
+
const fromBlock =
|
|
86
|
+
opts.fromBlock != null ? BigInt(opts.fromBlock) : preset.fromBlock;
|
|
82
87
|
const config = {
|
|
83
88
|
preset: preset.preset,
|
|
84
89
|
registryAddress: opts.registryAddress ?? preset.registryAddress,
|
|
85
90
|
chainRpcUrl: preset.chainRpcUrl,
|
|
86
91
|
brokerUrl: preset.brokerUrl,
|
|
87
92
|
mqttAuthServiceUrl: preset.mqttAuthServiceUrl,
|
|
88
|
-
fromBlock:
|
|
93
|
+
fromBlock: fromBlock.toString(),
|
|
89
94
|
};
|
|
90
95
|
if (name === "local" && !config.registryAddress) {
|
|
91
96
|
// Placeholder until chain deploy; operator must set after forge deploy.
|
|
@@ -120,8 +125,9 @@ export function loadOperator(home = clankerHome()) {
|
|
|
120
125
|
}
|
|
121
126
|
|
|
122
127
|
/**
|
|
123
|
-
* Write operator profile (label + owner + key pointer). Never stores hex keys.
|
|
124
|
-
*
|
|
128
|
+
* Write operator profile (label + owner + optional key pointer). Never stores hex keys.
|
|
129
|
+
* Omit `key` (or pass null) for a read-only profile; mint/revoke still need a pointer later.
|
|
130
|
+
* @param {{ label: string, owner: string, key?: { type: 'env'|'keyFile', value?: string }|null }} op
|
|
125
131
|
* @param {string} [home]
|
|
126
132
|
*/
|
|
127
133
|
export function writeOperator(op, home = clankerHome()) {
|
|
@@ -130,8 +136,10 @@ export function writeOperator(op, home = clankerHome()) {
|
|
|
130
136
|
const out = {
|
|
131
137
|
label: op.label,
|
|
132
138
|
owner: op.owner,
|
|
133
|
-
key: op.key ?? { type: "env", value: "OPERATOR_PRIVATE_KEY" },
|
|
134
139
|
};
|
|
140
|
+
if (op.key != null) {
|
|
141
|
+
out.key = op.key;
|
|
142
|
+
}
|
|
135
143
|
writeFileSync(path, `${JSON.stringify(out, null, 2)}\n`, { mode: 0o600 });
|
|
136
144
|
return { path, operator: out };
|
|
137
145
|
}
|
|
@@ -182,7 +190,7 @@ export function resolveNetwork(argv = [], opts = {}) {
|
|
|
182
190
|
}
|
|
183
191
|
|
|
184
192
|
if (fromBlock == null) {
|
|
185
|
-
fromBlock = isLocalRpc(rpc) ? 0n :
|
|
193
|
+
fromBlock = isLocalRpc(rpc) ? 0n : SEPOLIA_FAST_FROM_BLOCK;
|
|
186
194
|
}
|
|
187
195
|
|
|
188
196
|
return {
|
package/lib/resolve.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { getAddress } from "viem";
|
|
6
7
|
import { privateKeyToAccount } from "viem/accounts";
|
|
7
8
|
import {
|
|
8
9
|
ANVIL_DEFAULT_PRIVATE_KEY,
|
|
@@ -174,8 +175,63 @@ export function resolveForRead(argv = [], opts = {}) {
|
|
|
174
175
|
if (!network.registry || !/^0x[0-9a-fA-F]{40}$/.test(network.registry)) {
|
|
175
176
|
throw new Error(
|
|
176
177
|
"REGISTRY_ADDRESS or --registry is required (0x + 40 hex). " +
|
|
177
|
-
"Run `clanker init --preset sepolia` or set registry after
|
|
178
|
+
"Run `clanker init --preset sepolia` or `clanker setup`, or set registry after deploy.",
|
|
178
179
|
);
|
|
179
180
|
}
|
|
180
181
|
return network;
|
|
181
182
|
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Resolve address for read-only commands.
|
|
186
|
+
* Order: --address > signing key > operator.json owner > error (point at setup).
|
|
187
|
+
*
|
|
188
|
+
* @param {string[]} argv
|
|
189
|
+
* @param {{ home?: string, env?: NodeJS.ProcessEnv }} [opts]
|
|
190
|
+
* @returns {{ address: string, source: string, network: object }}
|
|
191
|
+
*/
|
|
192
|
+
export function resolveReadIdentity(argv = [], opts = {}) {
|
|
193
|
+
const network = resolveForRead(argv, opts);
|
|
194
|
+
const home = opts.home ?? network.home;
|
|
195
|
+
|
|
196
|
+
let addressFlag = null;
|
|
197
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
198
|
+
if (argv[i] === "--address" && argv[i + 1] && !argv[i + 1].startsWith("--")) {
|
|
199
|
+
addressFlag = argv[++i];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (addressFlag) {
|
|
203
|
+
return {
|
|
204
|
+
address: getAddress(addressFlag),
|
|
205
|
+
source: "--address",
|
|
206
|
+
network,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const resolved = resolveOperatorKey(argv, { ...opts, home });
|
|
212
|
+
return {
|
|
213
|
+
address: resolved.address,
|
|
214
|
+
source: resolved.source,
|
|
215
|
+
network: resolved.network,
|
|
216
|
+
};
|
|
217
|
+
} catch (err) {
|
|
218
|
+
const msg = err?.message ?? String(err);
|
|
219
|
+
if (!/private key required|Anvil account #0|Key file not found|Profile keyFile/i.test(msg)) {
|
|
220
|
+
throw err;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const operator = loadOperator(home);
|
|
225
|
+
if (operator?.owner && /^0x[0-9a-fA-F]{40}$/.test(operator.owner)) {
|
|
226
|
+
return {
|
|
227
|
+
address: getAddress(operator.owner),
|
|
228
|
+
source: "profile owner",
|
|
229
|
+
network,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
throw new Error(
|
|
234
|
+
"No operator identity configured. Run `clanker setup`, pass --address 0x…, " +
|
|
235
|
+
"or set a signing key (OPERATOR_PRIVATE_KEY / --key-file / operator.json key pointer).",
|
|
236
|
+
);
|
|
237
|
+
}
|