@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.
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Operator key resolution + Anvil guard for non-local RPCs.
3
+ */
4
+
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { getAddress } from "viem";
7
+ import { privateKeyToAccount } from "viem/accounts";
8
+ import {
9
+ ANVIL_DEFAULT_PRIVATE_KEY,
10
+ isLocalRpc,
11
+ loadOperator,
12
+ resolveNetwork,
13
+ } from "./profile.mjs";
14
+
15
+ /**
16
+ * Normalize a hex private key (with or without 0x).
17
+ * @param {string} raw
18
+ * @returns {`0x${string}`}
19
+ */
20
+ export function normalizePrivateKey(raw) {
21
+ const s = String(raw ?? "").trim();
22
+ if (!s) throw new Error("Empty private key");
23
+ const hex = s.startsWith("0x") ? s : `0x${s}`;
24
+ if (!/^0x[0-9a-fA-F]{64}$/.test(hex)) {
25
+ throw new Error("Private key must be 0x + 64 hex characters");
26
+ }
27
+ return /** @type {`0x${string}`} */ (hex.toLowerCase());
28
+ }
29
+
30
+ export function isAnvilDefaultKey(key) {
31
+ try {
32
+ return normalizePrivateKey(key) === ANVIL_DEFAULT_PRIVATE_KEY.toLowerCase();
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Parse --key / --key-file from argv (does not apply Anvil default).
40
+ * @param {string[]} argv
41
+ * @returns {{ key: string|null, keyFile: string|null, usedExplicitKey: boolean }}
42
+ */
43
+ export function parseKeyFlags(argv) {
44
+ let key = null;
45
+ let keyFile = null;
46
+ for (let i = 0; i < argv.length; i += 1) {
47
+ const a = argv[i];
48
+ if (a === "--key" && argv[i + 1]) {
49
+ key = argv[++i];
50
+ } else if (a === "--key-file" && argv[i + 1]) {
51
+ keyFile = argv[++i];
52
+ }
53
+ }
54
+ return { key, keyFile, usedExplicitKey: Boolean(key || keyFile) };
55
+ }
56
+
57
+ /**
58
+ * Map how the key was resolved into a durable operator.json pointer.
59
+ * Raw `--key` cannot be re-read later; persist env so the next command expects OPERATOR_PRIVATE_KEY.
60
+ *
61
+ * @param {{ source: string, keyFilePath?: string|null }} opts
62
+ * @returns {{ type: 'env'|'keyFile', value: string }}
63
+ */
64
+ export function keyPointerFromSource(opts) {
65
+ const source = opts.source ?? "";
66
+ if (source.startsWith("--key-file ")) {
67
+ return { type: "keyFile", value: source.slice("--key-file ".length) };
68
+ }
69
+ if (source.startsWith("profile keyFile ")) {
70
+ return { type: "keyFile", value: source.slice("profile keyFile ".length) };
71
+ }
72
+ if (opts.keyFilePath) {
73
+ return { type: "keyFile", value: opts.keyFilePath };
74
+ }
75
+ if (source.startsWith("profile env ")) {
76
+ return { type: "env", value: source.slice("profile env ".length) };
77
+ }
78
+ return { type: "env", value: "OPERATOR_PRIVATE_KEY" };
79
+ }
80
+
81
+ /**
82
+ * Resolve the operator signing key with Anvil guard.
83
+ *
84
+ * Precedence: --key > --key-file > OPERATOR_PRIVATE_KEY > profile keyFile > profile env > Anvil (local only).
85
+ *
86
+ * @param {string[]} argv
87
+ * @param {{ home?: string, env?: NodeJS.ProcessEnv, rpc?: string, requireRegistry?: boolean }} [opts]
88
+ * @returns {{ key: `0x${string}`, address: string, source: string, keyPointer: { type: string, value: string }, network: object }}
89
+ */
90
+ export function resolveOperatorKey(argv = [], opts = {}) {
91
+ const env = opts.env ?? process.env;
92
+ const network = resolveNetwork(argv, { home: opts.home, env });
93
+ const rpc = opts.rpc ?? network.rpc;
94
+ const flags = parseKeyFlags(argv);
95
+ const operator = loadOperator(opts.home ?? network.home);
96
+ const requireRegistry = opts.requireRegistry !== false;
97
+
98
+ let key = null;
99
+ let source = null;
100
+ let keyFilePath = null;
101
+
102
+ if (flags.key) {
103
+ key = normalizePrivateKey(flags.key);
104
+ source = "--key";
105
+ } else if (flags.keyFile || network.keyFile) {
106
+ const path = flags.keyFile || network.keyFile;
107
+ if (!existsSync(path)) {
108
+ throw new Error(`Key file not found: ${path}`);
109
+ }
110
+ key = normalizePrivateKey(readFileSync(path, "utf8").split(/\r?\n/)[0]);
111
+ keyFilePath = path;
112
+ source = `--key-file ${path}`;
113
+ } else if (env.OPERATOR_PRIVATE_KEY) {
114
+ key = normalizePrivateKey(env.OPERATOR_PRIVATE_KEY);
115
+ source = "OPERATOR_PRIVATE_KEY";
116
+ } else if (operator?.key?.type === "keyFile" && operator.key.value) {
117
+ const path = operator.key.value;
118
+ if (!existsSync(path)) {
119
+ throw new Error(`Profile keyFile not found: ${path}`);
120
+ }
121
+ key = normalizePrivateKey(readFileSync(path, "utf8").split(/\r?\n/)[0]);
122
+ keyFilePath = path;
123
+ source = `profile keyFile ${path}`;
124
+ } else if (operator?.key?.type === "env" && operator.key.value && env[operator.key.value]) {
125
+ key = normalizePrivateKey(env[operator.key.value]);
126
+ source = `profile env ${operator.key.value}`;
127
+ }
128
+
129
+ const local = isLocalRpc(rpc);
130
+
131
+ if (!key) {
132
+ if (local) {
133
+ key = normalizePrivateKey(ANVIL_DEFAULT_PRIVATE_KEY);
134
+ source = "anvil-default (local RPC)";
135
+ } else {
136
+ throw new Error(
137
+ "Operator private key required for non-local RPC. Set OPERATOR_PRIVATE_KEY, " +
138
+ "pass --key / --key-file, or configure ~/.clanker/operator.json key pointer. " +
139
+ "Anvil account #0 is not used on public networks.",
140
+ );
141
+ }
142
+ }
143
+
144
+ if (!local && isAnvilDefaultKey(key)) {
145
+ throw new Error(
146
+ "Refusing Anvil account #0 key on a non-local RPC. " +
147
+ "Use a real OPERATOR_PRIVATE_KEY / --key for Sepolia (or any public chain).",
148
+ );
149
+ }
150
+
151
+ if (requireRegistry && (!network.registry || !/^0x[0-9a-fA-F]{40}$/.test(network.registry))) {
152
+ throw new Error(
153
+ "REGISTRY_ADDRESS or --registry is required (0x + 40 hex). " +
154
+ "Run `clanker init --preset sepolia` or set registry after `clanker chain deploy`.",
155
+ );
156
+ }
157
+
158
+ const account = privateKeyToAccount(key);
159
+ const keyPointer = keyPointerFromSource({ source, keyFilePath });
160
+ return {
161
+ key,
162
+ address: account.address,
163
+ source,
164
+ keyPointer,
165
+ network: { ...network, rpc },
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Like resolveOperatorKey but for read-only commands that may not need a key
171
+ * (e.g. whoami can use --address). Still resolves network.
172
+ */
173
+ export function resolveForRead(argv = [], opts = {}) {
174
+ const network = resolveNetwork(argv, opts);
175
+ if (!network.registry || !/^0x[0-9a-fA-F]{40}$/.test(network.registry)) {
176
+ throw new Error(
177
+ "REGISTRY_ADDRESS or --registry is required (0x + 40 hex). " +
178
+ "Run `clanker init --preset sepolia` or `clanker setup`, or set registry after deploy.",
179
+ );
180
+ }
181
+ return network;
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
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Local identity hints for `clanker setup` (no secrets printed).
3
+ */
4
+
5
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
6
+ import { basename, join } from "node:path";
7
+ import { spawnSync } from "node:child_process";
8
+ import { privateKeyToAccount } from "viem/accounts";
9
+ import {
10
+ ANVIL_DEFAULT_ADDRESS,
11
+ clankerHome,
12
+ loadConfig,
13
+ loadOperator,
14
+ openclawKeysDir,
15
+ } from "./profile.mjs";
16
+ import { normalizePrivateKey } from "./resolve.mjs";
17
+
18
+ /** Faster Sepolia log floor for public RPC (registry floor remains SEPOLIA_FROM_BLOCK). */
19
+ export const SEPOLIA_FAST_FROM_BLOCK = 46_000_000n;
20
+
21
+ /**
22
+ * Parse `cast wallet list` stdout into account names.
23
+ * @param {string} stdout
24
+ * @returns {string[]}
25
+ */
26
+ export function parseCastWalletList(stdout) {
27
+ const names = [];
28
+ for (const line of String(stdout ?? "").split(/\r?\n/)) {
29
+ const trimmed = line.trim();
30
+ if (!trimmed) continue;
31
+ // Formats: "name (Local)" or "0xname (Local)" or just "name"
32
+ const m = trimmed.match(/^(\S+)/);
33
+ if (!m) continue;
34
+ let name = m[1];
35
+ if (name.startsWith("0x") && name.length > 2 && !/^0x[a-fA-F0-9]{40}$/.test(name)) {
36
+ // cast sometimes prefixes 0x to the account name display
37
+ name = name.slice(2);
38
+ }
39
+ if (/^0x[a-fA-F0-9]{40}$/.test(name)) continue;
40
+ names.push(name);
41
+ }
42
+ return [...new Set(names)];
43
+ }
44
+
45
+ /**
46
+ * @param {{ castBin?: string, spawn?: typeof spawnSync }} [opts]
47
+ * @returns {{ available: boolean, accounts: string[] }}
48
+ */
49
+ export function listFoundryAccounts(opts = {}) {
50
+ const spawn = opts.spawn ?? spawnSync;
51
+ const castBin = opts.castBin ?? "cast";
52
+ const result = spawn(castBin, ["wallet", "list"], {
53
+ encoding: "utf8",
54
+ shell: false,
55
+ });
56
+ if (result.error || result.status !== 0) {
57
+ return { available: false, accounts: [] };
58
+ }
59
+ return { available: true, accounts: parseCastWalletList(result.stdout ?? "") };
60
+ }
61
+
62
+ /**
63
+ * Bot key basenames under ~/.openclaw/keys (context only).
64
+ * @param {{ openclawDir?: string }} [opts]
65
+ * @returns {string[]}
66
+ */
67
+ export function listOpenclawBotKeys(opts = {}) {
68
+ const dir = opts.openclawDir ?? openclawKeysDir();
69
+ if (!existsSync(dir)) return [];
70
+ return readdirSync(dir)
71
+ .filter((f) => f.endsWith(".key"))
72
+ .map((f) => basename(f, ".key"))
73
+ .sort();
74
+ }
75
+
76
+ /**
77
+ * Derive address from a key file (first line). Does not log the key.
78
+ * @param {string} path
79
+ * @returns {string}
80
+ */
81
+ export function addressFromKeyFile(path) {
82
+ if (!existsSync(path)) throw new Error(`Key file not found: ${path}`);
83
+ const key = normalizePrivateKey(readFileSync(path, "utf8").split(/\r?\n/)[0]);
84
+ return privateKeyToAccount(key).address;
85
+ }
86
+
87
+ /**
88
+ * Derive address from OPERATOR_PRIVATE_KEY (or named env).
89
+ * @param {NodeJS.ProcessEnv} [env]
90
+ * @param {string} [envName]
91
+ * @returns {string|null}
92
+ */
93
+ export function addressFromEnv(env = process.env, envName = "OPERATOR_PRIVATE_KEY") {
94
+ const raw = env[envName];
95
+ if (!raw) return null;
96
+ const key = normalizePrivateKey(raw);
97
+ return privateKeyToAccount(key).address;
98
+ }
99
+
100
+ /**
101
+ * Snapshot of local hints for setup UX.
102
+ * @param {{ home?: string, env?: NodeJS.ProcessEnv, openclawDir?: string, castBin?: string, spawn?: typeof spawnSync }} [opts]
103
+ */
104
+ export function detectSetupHints(opts = {}) {
105
+ const env = opts.env ?? process.env;
106
+ const home = opts.home ?? clankerHome(env);
107
+ const config = loadConfig(home);
108
+ const operator = loadOperator(home);
109
+ const foundry = listFoundryAccounts({
110
+ castBin: opts.castBin,
111
+ spawn: opts.spawn,
112
+ });
113
+ const openclawBots = listOpenclawBotKeys({ openclawDir: opts.openclawDir });
114
+ let envAddress = null;
115
+ try {
116
+ envAddress = addressFromEnv(env);
117
+ } catch {
118
+ envAddress = null;
119
+ }
120
+
121
+ return {
122
+ home,
123
+ configPath: join(home, "config.json"),
124
+ operatorPath: join(home, "operator.json"),
125
+ hasConfig: Boolean(config),
126
+ hasOperator: Boolean(operator),
127
+ config,
128
+ operator,
129
+ hasOperatorPrivateKeyEnv: Boolean(env.OPERATOR_PRIVATE_KEY),
130
+ envAddress,
131
+ foundryAvailable: foundry.available,
132
+ foundryAccounts: foundry.accounts,
133
+ openclawBots,
134
+ anvilDefaultAddress: ANVIL_DEFAULT_ADDRESS,
135
+ };
136
+ }