@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
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only identity queries via contract storage + event logs.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createPublicClient,
|
|
7
|
+
defineChain,
|
|
8
|
+
getAddress,
|
|
9
|
+
http,
|
|
10
|
+
keccak256,
|
|
11
|
+
toBytes,
|
|
12
|
+
parseAbiItem,
|
|
13
|
+
} from "viem";
|
|
14
|
+
import { clankerIdentityAbi } from "./clanker-identity-abi.mjs";
|
|
15
|
+
|
|
16
|
+
/** Match identity-service EVM_LOG_CHUNK_BLOCKS — public RPCs reject wide ranges. */
|
|
17
|
+
export const LOG_CHUNK_BLOCKS = 2000n;
|
|
18
|
+
|
|
19
|
+
export function labelToId(label) {
|
|
20
|
+
return keccak256(toBytes(label));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const OPERATOR_REGISTERED = parseAbiItem(
|
|
24
|
+
"event OperatorRegistered(bytes32 indexed id, address indexed owner, string label)",
|
|
25
|
+
);
|
|
26
|
+
const OPERATOR_TRANSFERRED = parseAbiItem(
|
|
27
|
+
"event OperatorTransferred(bytes32 indexed id, address indexed oldOwner, address indexed newOwner)",
|
|
28
|
+
);
|
|
29
|
+
const BOT_REGISTERED = parseAbiItem(
|
|
30
|
+
"event BotRegistered(bytes32 indexed id, bytes32 indexed operatorId, address indexed botKey, string label)",
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} rpc
|
|
35
|
+
*/
|
|
36
|
+
export async function publicClientFromRpc(rpc) {
|
|
37
|
+
const transport = http(rpc);
|
|
38
|
+
const bare = createPublicClient({ transport });
|
|
39
|
+
const id = await bare.getChainId();
|
|
40
|
+
const chain = defineChain({
|
|
41
|
+
id,
|
|
42
|
+
name: `clanker-chain-${id}`,
|
|
43
|
+
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
|
44
|
+
rpcUrls: { default: { http: [rpc] } },
|
|
45
|
+
});
|
|
46
|
+
return createPublicClient({ chain, transport });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* eth_getLogs in fixed-size block chunks (public RPC safe).
|
|
51
|
+
*
|
|
52
|
+
* @param {{ getLogs: Function, getBlockNumber?: Function }} pub
|
|
53
|
+
* @param {object} params — viem getLogs params; fromBlock required; toBlock may be "latest"
|
|
54
|
+
* @param {{ chunkBlocks?: bigint }} [opts]
|
|
55
|
+
*/
|
|
56
|
+
export async function getLogsChunked(pub, params, opts = {}) {
|
|
57
|
+
const chunkBlocks = opts.chunkBlocks ?? LOG_CHUNK_BLOCKS;
|
|
58
|
+
let fromBlock = params.fromBlock ?? 0n;
|
|
59
|
+
if (typeof fromBlock === "number") fromBlock = BigInt(fromBlock);
|
|
60
|
+
|
|
61
|
+
let toBlock = params.toBlock ?? "latest";
|
|
62
|
+
if (toBlock === "latest") {
|
|
63
|
+
if (typeof pub.getBlockNumber !== "function") {
|
|
64
|
+
throw new Error("getLogsChunked: pub.getBlockNumber required when toBlock is latest");
|
|
65
|
+
}
|
|
66
|
+
toBlock = await pub.getBlockNumber();
|
|
67
|
+
} else if (typeof toBlock === "number") {
|
|
68
|
+
toBlock = BigInt(toBlock);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (fromBlock > toBlock) return [];
|
|
72
|
+
|
|
73
|
+
const all = [];
|
|
74
|
+
let cursor = fromBlock;
|
|
75
|
+
while (cursor <= toBlock) {
|
|
76
|
+
const chunkEnd =
|
|
77
|
+
cursor + chunkBlocks - 1n > toBlock ? toBlock : cursor + chunkBlocks - 1n;
|
|
78
|
+
const chunk = await pub.getLogs({
|
|
79
|
+
...params,
|
|
80
|
+
fromBlock: cursor,
|
|
81
|
+
toBlock: chunkEnd,
|
|
82
|
+
});
|
|
83
|
+
all.push(...chunk);
|
|
84
|
+
cursor = chunkEnd + 1n;
|
|
85
|
+
}
|
|
86
|
+
return all;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {import('viem').PublicClient} pub
|
|
91
|
+
* @param {string} registry
|
|
92
|
+
* @param {string} label
|
|
93
|
+
*/
|
|
94
|
+
export async function readOperator(pub, registry, label) {
|
|
95
|
+
const id = labelToId(label);
|
|
96
|
+
const [owner, registeredAt, revokedAt] = await pub.readContract({
|
|
97
|
+
address: /** @type {`0x${string}`} */ (registry),
|
|
98
|
+
abi: clankerIdentityAbi,
|
|
99
|
+
functionName: "operators",
|
|
100
|
+
args: [id],
|
|
101
|
+
});
|
|
102
|
+
return {
|
|
103
|
+
label,
|
|
104
|
+
id,
|
|
105
|
+
owner,
|
|
106
|
+
registeredAt: BigInt(registeredAt),
|
|
107
|
+
revokedAt: BigInt(revokedAt),
|
|
108
|
+
active: registeredAt > 0n && revokedAt === 0n,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Preferred-label path: storage read, no logs. Requires current owner + active.
|
|
114
|
+
*
|
|
115
|
+
* @param {import('viem').PublicClient} pub
|
|
116
|
+
* @param {{ registry: string, label: string, owner: `0x${string}` }} opts
|
|
117
|
+
*/
|
|
118
|
+
export async function resolvePreferredOperator(pub, opts) {
|
|
119
|
+
const op = await readOperator(pub, opts.registry, opts.label);
|
|
120
|
+
if (op.registeredAt === 0n) {
|
|
121
|
+
return {
|
|
122
|
+
error: `Operator "${opts.label}" is not registered`,
|
|
123
|
+
candidates: [],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (!op.active) {
|
|
127
|
+
return {
|
|
128
|
+
error: `Operator "${opts.label}" is revoked`,
|
|
129
|
+
candidates: [],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const want = getAddress(opts.owner);
|
|
133
|
+
const got = getAddress(op.owner);
|
|
134
|
+
if (want !== got) {
|
|
135
|
+
return {
|
|
136
|
+
error: `Operator "${opts.label}" is owned by ${got}, not ${want}`,
|
|
137
|
+
candidates: [],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return { operator: op };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {import('viem').PublicClient} pub
|
|
145
|
+
* @param {string} registry
|
|
146
|
+
* @param {string} label
|
|
147
|
+
*/
|
|
148
|
+
export async function readBot(pub, registry, label) {
|
|
149
|
+
const id = labelToId(label);
|
|
150
|
+
const [operatorId, botKey, registeredAt, revokedAt] = await pub.readContract({
|
|
151
|
+
address: /** @type {`0x${string}`} */ (registry),
|
|
152
|
+
abi: clankerIdentityAbi,
|
|
153
|
+
functionName: "bots",
|
|
154
|
+
args: [id],
|
|
155
|
+
});
|
|
156
|
+
return {
|
|
157
|
+
label,
|
|
158
|
+
id,
|
|
159
|
+
operatorId,
|
|
160
|
+
botKey,
|
|
161
|
+
registeredAt: BigInt(registeredAt),
|
|
162
|
+
revokedAt: BigInt(revokedAt),
|
|
163
|
+
active: registeredAt > 0n && revokedAt === 0n,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Find operators currently owned by address (minted or transferred).
|
|
169
|
+
* @param {import('viem').PublicClient} pub
|
|
170
|
+
* @param {{ registry: string, owner: `0x${string}`, fromBlock?: bigint, chunkBlocks?: bigint }} opts
|
|
171
|
+
*/
|
|
172
|
+
export async function findOperatorsByOwner(pub, opts) {
|
|
173
|
+
const fromBlock = opts.fromBlock ?? 0n;
|
|
174
|
+
const chunkBlocks = opts.chunkBlocks ?? LOG_CHUNK_BLOCKS;
|
|
175
|
+
const registry = /** @type {`0x${string}`} */ (opts.registry);
|
|
176
|
+
const owner = getAddress(opts.owner);
|
|
177
|
+
|
|
178
|
+
const registeredLogs = await getLogsChunked(
|
|
179
|
+
pub,
|
|
180
|
+
{
|
|
181
|
+
address: registry,
|
|
182
|
+
event: OPERATOR_REGISTERED,
|
|
183
|
+
args: { owner },
|
|
184
|
+
fromBlock,
|
|
185
|
+
toBlock: "latest",
|
|
186
|
+
},
|
|
187
|
+
{ chunkBlocks },
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const transferredLogs = await getLogsChunked(
|
|
191
|
+
pub,
|
|
192
|
+
{
|
|
193
|
+
address: registry,
|
|
194
|
+
event: OPERATOR_TRANSFERRED,
|
|
195
|
+
args: { newOwner: owner },
|
|
196
|
+
fromBlock,
|
|
197
|
+
toBlock: "latest",
|
|
198
|
+
},
|
|
199
|
+
{ chunkBlocks },
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
/** @type {Map<string, { label?: string, id: `0x${string}`, tx?: string }>} */
|
|
203
|
+
const byId = new Map();
|
|
204
|
+
|
|
205
|
+
for (const log of registeredLogs) {
|
|
206
|
+
const id = log.args.id;
|
|
207
|
+
const label = log.args.label;
|
|
208
|
+
if (!id) continue;
|
|
209
|
+
byId.set(id.toLowerCase(), {
|
|
210
|
+
id,
|
|
211
|
+
label: label ?? undefined,
|
|
212
|
+
tx: log.transactionHash,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const log of transferredLogs) {
|
|
217
|
+
const id = log.args.id;
|
|
218
|
+
if (!id) continue;
|
|
219
|
+
const key = id.toLowerCase();
|
|
220
|
+
if (!byId.has(key)) {
|
|
221
|
+
byId.set(key, { id, tx: log.transactionHash });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Recover labels for transfer-only ids via OperatorRegistered filtered by id.
|
|
226
|
+
for (const entry of byId.values()) {
|
|
227
|
+
if (entry.label) continue;
|
|
228
|
+
const labelLogs = await getLogsChunked(
|
|
229
|
+
pub,
|
|
230
|
+
{
|
|
231
|
+
address: registry,
|
|
232
|
+
event: OPERATOR_REGISTERED,
|
|
233
|
+
args: { id: entry.id },
|
|
234
|
+
fromBlock,
|
|
235
|
+
toBlock: "latest",
|
|
236
|
+
},
|
|
237
|
+
{ chunkBlocks },
|
|
238
|
+
);
|
|
239
|
+
const last = labelLogs[labelLogs.length - 1];
|
|
240
|
+
if (last?.args?.label) {
|
|
241
|
+
entry.label = last.args.label;
|
|
242
|
+
entry.tx = entry.tx ?? last.transactionHash;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const results = [];
|
|
247
|
+
for (const entry of byId.values()) {
|
|
248
|
+
if (!entry.label) continue;
|
|
249
|
+
const [storageOwner, registeredAt, revokedAt] = await pub.readContract({
|
|
250
|
+
address: registry,
|
|
251
|
+
abi: clankerIdentityAbi,
|
|
252
|
+
functionName: "operators",
|
|
253
|
+
args: [entry.id],
|
|
254
|
+
});
|
|
255
|
+
if (getAddress(storageOwner) !== owner) continue;
|
|
256
|
+
results.push({
|
|
257
|
+
label: entry.label,
|
|
258
|
+
id: entry.id,
|
|
259
|
+
owner: storageOwner,
|
|
260
|
+
registeredAt: BigInt(registeredAt),
|
|
261
|
+
revokedAt: BigInt(revokedAt),
|
|
262
|
+
active: registeredAt > 0n && revokedAt === 0n,
|
|
263
|
+
mintTx: entry.tx,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return results;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Find bots under an operator via BotRegistered logs + storage refresh.
|
|
271
|
+
* @param {import('viem').PublicClient} pub
|
|
272
|
+
* @param {{ registry: string, operatorId: `0x${string}`, fromBlock?: bigint, chunkBlocks?: bigint }} opts
|
|
273
|
+
*/
|
|
274
|
+
export async function findBotsByOperator(pub, opts) {
|
|
275
|
+
const fromBlock = opts.fromBlock ?? 0n;
|
|
276
|
+
const chunkBlocks = opts.chunkBlocks ?? LOG_CHUNK_BLOCKS;
|
|
277
|
+
const registry = /** @type {`0x${string}`} */ (opts.registry);
|
|
278
|
+
|
|
279
|
+
const logs = await getLogsChunked(
|
|
280
|
+
pub,
|
|
281
|
+
{
|
|
282
|
+
address: registry,
|
|
283
|
+
event: BOT_REGISTERED,
|
|
284
|
+
args: { operatorId: opts.operatorId },
|
|
285
|
+
fromBlock,
|
|
286
|
+
toBlock: "latest",
|
|
287
|
+
},
|
|
288
|
+
{ chunkBlocks },
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const byLabel = new Map();
|
|
292
|
+
for (const log of logs) {
|
|
293
|
+
const label = log.args.label;
|
|
294
|
+
const id = log.args.id;
|
|
295
|
+
if (!label || !id) continue;
|
|
296
|
+
byLabel.set(label, {
|
|
297
|
+
label,
|
|
298
|
+
id,
|
|
299
|
+
botKeyAtMint: log.args.botKey,
|
|
300
|
+
blockNumber: log.blockNumber,
|
|
301
|
+
tx: log.transactionHash,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const results = [];
|
|
306
|
+
for (const entry of byLabel.values()) {
|
|
307
|
+
const [operatorId, botKey, registeredAt, revokedAt] = await pub.readContract({
|
|
308
|
+
address: registry,
|
|
309
|
+
abi: clankerIdentityAbi,
|
|
310
|
+
functionName: "bots",
|
|
311
|
+
args: [entry.id],
|
|
312
|
+
});
|
|
313
|
+
results.push({
|
|
314
|
+
label: entry.label,
|
|
315
|
+
id: entry.id,
|
|
316
|
+
operatorId,
|
|
317
|
+
botKey,
|
|
318
|
+
registeredAt: BigInt(registeredAt),
|
|
319
|
+
revokedAt: BigInt(revokedAt),
|
|
320
|
+
active: registeredAt > 0n && revokedAt === 0n,
|
|
321
|
+
mintTx: entry.tx,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return results;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Pure helper: pick a single operator label when profile/flag/list interact.
|
|
329
|
+
* Preferred labels must be active when present in the list.
|
|
330
|
+
*
|
|
331
|
+
* @param {{ operators: Array<{label: string, active: boolean}>, preferred?: string|null }} opts
|
|
332
|
+
* @returns {{ label: string } | { error: string, candidates: string[] }}
|
|
333
|
+
*/
|
|
334
|
+
export function pickOperatorLabel(opts) {
|
|
335
|
+
const active = opts.operators.filter((o) => o.active);
|
|
336
|
+
if (opts.preferred) {
|
|
337
|
+
const hit = opts.operators.find((o) => o.label === opts.preferred);
|
|
338
|
+
if (!hit) {
|
|
339
|
+
return {
|
|
340
|
+
error: `Operator "${opts.preferred}" not found for this owner`,
|
|
341
|
+
candidates: opts.operators.map((o) => o.label),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (!hit.active) {
|
|
345
|
+
return {
|
|
346
|
+
error: `Operator "${opts.preferred}" is revoked`,
|
|
347
|
+
candidates: active.map((o) => o.label),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
return { label: hit.label };
|
|
351
|
+
}
|
|
352
|
+
if (active.length === 1) return { label: active[0].label };
|
|
353
|
+
if (active.length === 0) {
|
|
354
|
+
return {
|
|
355
|
+
error: "No active operators for this address. Mint one with `clanker operator mint <label>`.",
|
|
356
|
+
candidates: [],
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
error:
|
|
361
|
+
"Multiple operators for this address; pass --operator <label> or set ~/.clanker/operator.json",
|
|
362
|
+
candidates: active.map((o) => o.label),
|
|
363
|
+
};
|
|
364
|
+
}
|
package/lib/keys.mjs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dual-write bot keys to ~/.openclaw/keys and ~/.clanker/keys.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, symlinkSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { clankerKeysDir, openclawKeysDir } from "./profile.mjs";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reject labels that are unsafe as a single path segment.
|
|
11
|
+
* @param {string} label
|
|
12
|
+
*/
|
|
13
|
+
export function assertSafeBotLabel(label) {
|
|
14
|
+
const s = String(label ?? "");
|
|
15
|
+
if (!s) {
|
|
16
|
+
throw new Error("Bot label must be non-empty");
|
|
17
|
+
}
|
|
18
|
+
if (s.includes("\0")) {
|
|
19
|
+
throw new Error("Bot label must not contain NUL");
|
|
20
|
+
}
|
|
21
|
+
if (s.includes("/") || s.includes("\\")) {
|
|
22
|
+
throw new Error("Bot label must not contain path separators");
|
|
23
|
+
}
|
|
24
|
+
if (s === "." || s === ".." || s.includes("..")) {
|
|
25
|
+
throw new Error('Bot label must not contain ".."');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Write bot private key before broadcasting registerBot.
|
|
31
|
+
* Primary path remains ~/.openclaw/keys for published plugins.
|
|
32
|
+
* Also writes (or symlinks) under ~/.clanker/keys (or CLANKER_KEY_DIR).
|
|
33
|
+
*
|
|
34
|
+
* @param {string} botLabel
|
|
35
|
+
* @param {string} botPrivateKey
|
|
36
|
+
* @param {{ env?: NodeJS.ProcessEnv, openclawKeysDir?: string, clankerKeysDir?: string }} [opts]
|
|
37
|
+
* @returns {{ openclawPath: string, clankerPath: string }}
|
|
38
|
+
*/
|
|
39
|
+
export function writeBotKeyFiles(botLabel, botPrivateKey, opts = {}) {
|
|
40
|
+
assertSafeBotLabel(botLabel);
|
|
41
|
+
const env = opts.env ?? process.env;
|
|
42
|
+
const openclawDir = opts.openclawKeysDir ?? openclawKeysDir();
|
|
43
|
+
const clankerDir = opts.clankerKeysDir ?? clankerKeysDir(env);
|
|
44
|
+
|
|
45
|
+
mkdirSync(openclawDir, { recursive: true });
|
|
46
|
+
mkdirSync(clankerDir, { recursive: true });
|
|
47
|
+
|
|
48
|
+
const openclawPath = join(openclawDir, `${botLabel}.key`);
|
|
49
|
+
const clankerPath = join(clankerDir, `${botLabel}.key`);
|
|
50
|
+
|
|
51
|
+
if (existsSync(openclawPath)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Key file already exists at ${openclawPath}; refusing to overwrite. ` +
|
|
54
|
+
`Remove it or choose a different bot label.`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if (existsSync(clankerPath)) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Key file already exists at ${clankerPath}; refusing to overwrite. ` +
|
|
60
|
+
`Remove it or choose a different bot label.`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const body = `${botPrivateKey}\n`;
|
|
65
|
+
writeFileSync(openclawPath, body, { flag: "wx", mode: 0o600 });
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
symlinkSync(openclawPath, clankerPath);
|
|
69
|
+
} catch {
|
|
70
|
+
// Symlink may fail on some FS; fall back to a second copy.
|
|
71
|
+
writeFileSync(clankerPath, body, { flag: "wx", mode: 0o600 });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { openclawPath, clankerPath };
|
|
75
|
+
}
|
package/lib/profile.mjs
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local operator profile under ~/.clanker (or CLANKER_HOME).
|
|
3
|
+
* Never stores raw private keys — only key pointers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
/** Anvil account #0 — local-dev only. */
|
|
11
|
+
export const ANVIL_DEFAULT_PRIVATE_KEY =
|
|
12
|
+
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
|
|
13
|
+
|
|
14
|
+
export const ANVIL_DEFAULT_ADDRESS = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
|
|
15
|
+
|
|
16
|
+
/** Closed-beta Base Sepolia registry (see docs/public-testnet-hub.md). */
|
|
17
|
+
export const SEPOLIA_REGISTRY = "0xD650467f9D7A20f37E55ec23Ca1c711598f97958";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Safe floor for Sepolia getLogs (before registry deploy). Documented so scans
|
|
21
|
+
* stay cheap; bump only if you redeploy the registry earlier.
|
|
22
|
+
*/
|
|
23
|
+
export const SEPOLIA_FROM_BLOCK = 35_000_000n;
|
|
24
|
+
|
|
25
|
+
export const PRESETS = {
|
|
26
|
+
local: {
|
|
27
|
+
preset: "local",
|
|
28
|
+
registryAddress: null,
|
|
29
|
+
chainRpcUrl: "http://127.0.0.1:8545",
|
|
30
|
+
brokerUrl: "mqtt://localhost:1883",
|
|
31
|
+
mqttAuthServiceUrl: "http://localhost:9090",
|
|
32
|
+
fromBlock: 0n,
|
|
33
|
+
},
|
|
34
|
+
sepolia: {
|
|
35
|
+
preset: "sepolia",
|
|
36
|
+
registryAddress: SEPOLIA_REGISTRY,
|
|
37
|
+
chainRpcUrl: "https://sepolia.base.org",
|
|
38
|
+
brokerUrl: "mqtts://mqtt.clanker-chain.com:8883",
|
|
39
|
+
mqttAuthServiceUrl: "https://mqtt-auth.clanker-chain.com",
|
|
40
|
+
fromBlock: SEPOLIA_FROM_BLOCK,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function clankerHome(env = process.env) {
|
|
45
|
+
return env.CLANKER_HOME ?? join(homedir(), ".clanker");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function configPath(home = clankerHome()) {
|
|
49
|
+
return join(home, "config.json");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function operatorPath(home = clankerHome()) {
|
|
53
|
+
return join(home, "operator.json");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function clankerKeysDir(env = process.env, home = clankerHome(env)) {
|
|
57
|
+
return env.CLANKER_KEY_DIR ?? join(home, "keys");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function openclawKeysDir() {
|
|
61
|
+
return join(homedir(), ".openclaw", "keys");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} presetName
|
|
66
|
+
* @param {{ force?: boolean, registryAddress?: string|null, home?: string, fromBlock?: bigint|string|number }} [opts]
|
|
67
|
+
*/
|
|
68
|
+
export function initProfile(presetName, opts = {}) {
|
|
69
|
+
const name = String(presetName || "").toLowerCase();
|
|
70
|
+
if (!PRESETS[name]) {
|
|
71
|
+
throw new Error(`Unknown preset "${presetName}". Use "local" or "sepolia".`);
|
|
72
|
+
}
|
|
73
|
+
const home = opts.home ?? clankerHome();
|
|
74
|
+
const path = configPath(home);
|
|
75
|
+
if (existsSync(path) && !opts.force) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Profile already exists at ${path}. Pass --force to overwrite, or edit the file.`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
mkdirSync(home, { recursive: true });
|
|
81
|
+
const preset = PRESETS[name];
|
|
82
|
+
const fromBlock =
|
|
83
|
+
opts.fromBlock != null ? BigInt(opts.fromBlock) : preset.fromBlock;
|
|
84
|
+
const config = {
|
|
85
|
+
preset: preset.preset,
|
|
86
|
+
registryAddress: opts.registryAddress ?? preset.registryAddress,
|
|
87
|
+
chainRpcUrl: preset.chainRpcUrl,
|
|
88
|
+
brokerUrl: preset.brokerUrl,
|
|
89
|
+
mqttAuthServiceUrl: preset.mqttAuthServiceUrl,
|
|
90
|
+
fromBlock: fromBlock.toString(),
|
|
91
|
+
};
|
|
92
|
+
if (name === "local" && !config.registryAddress) {
|
|
93
|
+
// Placeholder until chain deploy; operator must set after forge deploy.
|
|
94
|
+
config.registryAddress = null;
|
|
95
|
+
}
|
|
96
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
97
|
+
return { path, config };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {string} [home]
|
|
102
|
+
* @returns {object|null}
|
|
103
|
+
*/
|
|
104
|
+
export function loadConfig(home = clankerHome()) {
|
|
105
|
+
const path = configPath(home);
|
|
106
|
+
if (!existsSync(path)) return null;
|
|
107
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
108
|
+
if (raw.fromBlock != null && typeof raw.fromBlock !== "bigint") {
|
|
109
|
+
raw.fromBlock = BigInt(raw.fromBlock);
|
|
110
|
+
}
|
|
111
|
+
return raw;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {string} [home]
|
|
116
|
+
* @returns {object|null}
|
|
117
|
+
*/
|
|
118
|
+
export function loadOperator(home = clankerHome()) {
|
|
119
|
+
const path = operatorPath(home);
|
|
120
|
+
if (!existsSync(path)) return null;
|
|
121
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Write operator profile (label + owner + optional key pointer). Never stores hex keys.
|
|
126
|
+
* Omit `key` (or pass null) for a read-only profile; mint/revoke still need a pointer later.
|
|
127
|
+
* @param {{ label: string, owner: string, key?: { type: 'env'|'keyFile', value?: string }|null }} op
|
|
128
|
+
* @param {string} [home]
|
|
129
|
+
*/
|
|
130
|
+
export function writeOperator(op, home = clankerHome()) {
|
|
131
|
+
mkdirSync(home, { recursive: true });
|
|
132
|
+
const path = operatorPath(home);
|
|
133
|
+
const out = {
|
|
134
|
+
label: op.label,
|
|
135
|
+
owner: op.owner,
|
|
136
|
+
};
|
|
137
|
+
if (op.key != null) {
|
|
138
|
+
out.key = op.key;
|
|
139
|
+
}
|
|
140
|
+
writeFileSync(path, `${JSON.stringify(out, null, 2)}\n`, { mode: 0o600 });
|
|
141
|
+
return { path, operator: out };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Merge profile + env + argv overrides into a network view (no key yet).
|
|
146
|
+
* Precedence: argv flags > env > profile > preset defaults for local RPC.
|
|
147
|
+
*
|
|
148
|
+
* @param {string[]} argv
|
|
149
|
+
* @param {{ home?: string, env?: NodeJS.ProcessEnv }} [opts]
|
|
150
|
+
*/
|
|
151
|
+
export function resolveNetwork(argv = [], opts = {}) {
|
|
152
|
+
const env = opts.env ?? process.env;
|
|
153
|
+
const home = opts.home ?? clankerHome(env);
|
|
154
|
+
const config = loadConfig(home);
|
|
155
|
+
|
|
156
|
+
let rpc = env.CHAIN_RPC_URL ?? env.BASE_SEPOLIA_RPC_URL ?? null;
|
|
157
|
+
let registry = env.REGISTRY_ADDRESS ?? null;
|
|
158
|
+
let fromBlock = null;
|
|
159
|
+
let brokerUrl = null;
|
|
160
|
+
let mqttAuthServiceUrl = null;
|
|
161
|
+
let preset = null;
|
|
162
|
+
let keyFile = null;
|
|
163
|
+
let json = false;
|
|
164
|
+
let operatorLabel = null;
|
|
165
|
+
|
|
166
|
+
if (config) {
|
|
167
|
+
preset = config.preset ?? null;
|
|
168
|
+
rpc = rpc ?? config.chainRpcUrl ?? null;
|
|
169
|
+
registry = registry ?? config.registryAddress ?? null;
|
|
170
|
+
fromBlock = config.fromBlock != null ? BigInt(config.fromBlock) : null;
|
|
171
|
+
brokerUrl = config.brokerUrl ?? null;
|
|
172
|
+
mqttAuthServiceUrl = config.mqttAuthServiceUrl ?? null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
176
|
+
const a = argv[i];
|
|
177
|
+
if (a === "--rpc" && argv[i + 1]) rpc = argv[++i];
|
|
178
|
+
else if (a === "--registry" && argv[i + 1]) registry = argv[++i];
|
|
179
|
+
else if (a === "--key-file" && argv[i + 1]) keyFile = argv[++i];
|
|
180
|
+
else if (a === "--operator" && argv[i + 1]) operatorLabel = argv[++i];
|
|
181
|
+
else if (a === "--from-block" && argv[i + 1]) fromBlock = BigInt(argv[++i]);
|
|
182
|
+
else if (a === "--json") json = true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!rpc) {
|
|
186
|
+
rpc = "http://127.0.0.1:8545";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (fromBlock == null) {
|
|
190
|
+
fromBlock = isLocalRpc(rpc) ? 0n : SEPOLIA_FROM_BLOCK;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
rpc,
|
|
195
|
+
registry,
|
|
196
|
+
fromBlock,
|
|
197
|
+
brokerUrl,
|
|
198
|
+
mqttAuthServiceUrl,
|
|
199
|
+
preset,
|
|
200
|
+
keyFile,
|
|
201
|
+
json,
|
|
202
|
+
operatorLabel,
|
|
203
|
+
config,
|
|
204
|
+
home,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function isLocalRpc(rpc) {
|
|
209
|
+
if (!rpc) return false;
|
|
210
|
+
try {
|
|
211
|
+
const u = new URL(rpc);
|
|
212
|
+
const host = u.hostname.toLowerCase();
|
|
213
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
214
|
+
} catch {
|
|
215
|
+
return /127\.0\.0\.1|localhost/.test(rpc);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Harness snippet for OpenClaw channels.mqtt from network + ids.
|
|
221
|
+
*/
|
|
222
|
+
export function harnessSnippet({ botId, operatorId, network }) {
|
|
223
|
+
return {
|
|
224
|
+
enabled: true,
|
|
225
|
+
botId,
|
|
226
|
+
operatorId,
|
|
227
|
+
brokerUrl: network.brokerUrl ?? "mqtt://localhost:1883",
|
|
228
|
+
chainRpcUrl: network.rpc,
|
|
229
|
+
registryAddress: network.registry,
|
|
230
|
+
mqttAuthServiceUrl: network.mqttAuthServiceUrl ?? "http://localhost:9090",
|
|
231
|
+
};
|
|
232
|
+
}
|