@clanker-chain/clanker-cli 2026.9.8 → 2026.9.12
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 +239 -21
- package/lib/clanker-identity-abi.mjs +18 -0
- package/lib/doctor.mjs +91 -4
- package/lib/fund.mjs +250 -0
- package/lib/identity-query.mjs +1 -1
- package/lib/mint-budget.mjs +237 -0
- package/lib/openclaw-wire.mjs +43 -2
- package/lib/operator-key.mjs +58 -0
- package/lib/pair.mjs +252 -0
- package/lib/setup.mjs +157 -39
- package/package.json +5 -2
package/lib/fund.mjs
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `clanker fund` — print mint ETH budget, open faucet, poll until funded.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { getAddress } from "viem";
|
|
7
|
+
import { clankerHome, loadConfig, loadOperator, isLocalRpc } from "./profile.mjs";
|
|
8
|
+
import { publicClientFromRpc } from "./identity-query.mjs";
|
|
9
|
+
import {
|
|
10
|
+
assessMintBudget,
|
|
11
|
+
budgetToJson,
|
|
12
|
+
formatBudgetSummary,
|
|
13
|
+
formatEthTrim,
|
|
14
|
+
} from "./mint-budget.mjs";
|
|
15
|
+
import { c, nextHint } from "./ui.mjs";
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_FUND_TIMEOUT_MS = 5 * 60 * 1000;
|
|
18
|
+
export const DEFAULT_FUND_POLL_MS = 5000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Best-effort open URL in the default browser. Never throws.
|
|
22
|
+
* @param {string} url
|
|
23
|
+
* @param {{ platform?: string, spawnImpl?: typeof spawn }} [opts]
|
|
24
|
+
* @returns {Promise<boolean>}
|
|
25
|
+
*/
|
|
26
|
+
export async function openUrl(url, opts = {}) {
|
|
27
|
+
const platform = opts.platform ?? process.platform;
|
|
28
|
+
const spawnImpl = opts.spawnImpl ?? spawn;
|
|
29
|
+
try {
|
|
30
|
+
if (platform === "darwin") {
|
|
31
|
+
spawnImpl("open", [url], { stdio: "ignore", detached: true }).unref();
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (platform === "win32") {
|
|
35
|
+
spawnImpl("cmd", ["/c", "start", "", url], {
|
|
36
|
+
stdio: "ignore",
|
|
37
|
+
detached: true,
|
|
38
|
+
}).unref();
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
spawnImpl("xdg-open", [url], { stdio: "ignore", detached: true }).unref();
|
|
42
|
+
return true;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {string[]} argv
|
|
50
|
+
*/
|
|
51
|
+
export function parseFundFlags(argv) {
|
|
52
|
+
let timeoutMs = DEFAULT_FUND_TIMEOUT_MS;
|
|
53
|
+
let pollMs = DEFAULT_FUND_POLL_MS;
|
|
54
|
+
let noOpen = false;
|
|
55
|
+
let json = false;
|
|
56
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
57
|
+
const a = argv[i];
|
|
58
|
+
if (a === "--no-open") noOpen = true;
|
|
59
|
+
else if (a === "--json") json = true;
|
|
60
|
+
else if (a === "--timeout" && argv[i + 1]) {
|
|
61
|
+
timeoutMs = Number(argv[++i]);
|
|
62
|
+
} else if (a === "--poll" && argv[i + 1]) {
|
|
63
|
+
pollMs = Number(argv[++i]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
67
|
+
throw new Error("--timeout must be a non-negative number (ms)");
|
|
68
|
+
}
|
|
69
|
+
if (!Number.isFinite(pollMs) || pollMs < 100) {
|
|
70
|
+
throw new Error("--poll must be >= 100 (ms)");
|
|
71
|
+
}
|
|
72
|
+
return { timeoutMs, pollMs, noOpen, json };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {string[]} argv
|
|
77
|
+
* @param {{
|
|
78
|
+
* home?: string,
|
|
79
|
+
* env?: NodeJS.ProcessEnv,
|
|
80
|
+
* publicClient?: { readContract: Function, getBalance: Function },
|
|
81
|
+
* openUrlImpl?: typeof openUrl,
|
|
82
|
+
* sleep?: (ms: number) => Promise<void>,
|
|
83
|
+
* now?: () => number,
|
|
84
|
+
* }} [opts]
|
|
85
|
+
*/
|
|
86
|
+
export async function runFund(argv = [], opts = {}) {
|
|
87
|
+
const flags = parseFundFlags(argv);
|
|
88
|
+
const env = opts.env ?? process.env;
|
|
89
|
+
const home = opts.home ?? clankerHome(env);
|
|
90
|
+
const config = loadConfig(home);
|
|
91
|
+
const operator = loadOperator(home);
|
|
92
|
+
const sleep =
|
|
93
|
+
opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
94
|
+
const now = opts.now ?? (() => Date.now());
|
|
95
|
+
const openUrlImpl = opts.openUrlImpl ?? openUrl;
|
|
96
|
+
|
|
97
|
+
if (!config) {
|
|
98
|
+
throw new Error("config.json missing — run clanker setup");
|
|
99
|
+
}
|
|
100
|
+
if (!operator?.owner || !/^0x[0-9a-fA-F]{40}$/.test(operator.owner)) {
|
|
101
|
+
throw new Error("operator.json incomplete — run clanker setup");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const rpc = config.chainRpcUrl ?? "";
|
|
105
|
+
const registry = config.registryAddress;
|
|
106
|
+
const owner = getAddress(operator.owner);
|
|
107
|
+
|
|
108
|
+
if (isLocalRpc(rpc)) {
|
|
109
|
+
const payload = {
|
|
110
|
+
ok: true,
|
|
111
|
+
funded: true,
|
|
112
|
+
local: true,
|
|
113
|
+
owner,
|
|
114
|
+
message: "Anvil is prefunded — no faucet needed",
|
|
115
|
+
};
|
|
116
|
+
if (flags.json) {
|
|
117
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
118
|
+
} else {
|
|
119
|
+
console.log(c.bold("clanker fund"));
|
|
120
|
+
console.log("");
|
|
121
|
+
console.log(c.green("Anvil is prefunded — no faucet needed."));
|
|
122
|
+
nextHint(["clanker doctor", "clanker whoami"]);
|
|
123
|
+
}
|
|
124
|
+
return { exitCode: 0, ...payload };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!registry || !/^0x[0-9a-fA-F]{40}$/.test(registry)) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
"registry missing — run clanker setup --preset sepolia (or set after local deploy)",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const pub = opts.publicClient ?? (await publicClientFromRpc(rpc));
|
|
134
|
+
|
|
135
|
+
const assess = () =>
|
|
136
|
+
assessMintBudget({
|
|
137
|
+
pub,
|
|
138
|
+
registry,
|
|
139
|
+
owner,
|
|
140
|
+
rpc,
|
|
141
|
+
operatorLabel: operator.label,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
let budget = await assess();
|
|
145
|
+
|
|
146
|
+
if (flags.json && budget.funded) {
|
|
147
|
+
console.log(
|
|
148
|
+
JSON.stringify({ ok: true, ...budgetToJson(budget) }, null, 2),
|
|
149
|
+
);
|
|
150
|
+
return { exitCode: 0, funded: true, budget };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!flags.json) {
|
|
154
|
+
console.log(c.bold("clanker fund"));
|
|
155
|
+
console.log("");
|
|
156
|
+
console.log(`owner: ${owner}`);
|
|
157
|
+
console.log(`label: ${operator.label ?? "(none)"}`);
|
|
158
|
+
console.log(`registry: ${registry}`);
|
|
159
|
+
console.log(`budget: ${formatBudgetSummary(budget)}`);
|
|
160
|
+
console.log(`faucet: ${budget.faucetUrl}`);
|
|
161
|
+
console.log(`backup: ${budget.backupFaucetUrl}`);
|
|
162
|
+
console.log("");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (budget.funded) {
|
|
166
|
+
if (!flags.json) {
|
|
167
|
+
console.log(c.green("Already funded for remaining mint fees + gas."));
|
|
168
|
+
nextHint([
|
|
169
|
+
"clanker doctor",
|
|
170
|
+
`clanker operator mint ${operator.label ?? "<label>"} --yes`,
|
|
171
|
+
"clanker bot mint <bot_label> --yes",
|
|
172
|
+
]);
|
|
173
|
+
}
|
|
174
|
+
return { exitCode: 0, funded: true, budget };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!flags.noOpen) {
|
|
178
|
+
const opened = await openUrlImpl(budget.faucetUrl);
|
|
179
|
+
if (!flags.json) {
|
|
180
|
+
if (opened) {
|
|
181
|
+
console.log(c.dim(`Opened faucet in browser (paste ${owner}).`));
|
|
182
|
+
} else {
|
|
183
|
+
console.log(
|
|
184
|
+
c.dim(`Could not open browser — visit ${budget.faucetUrl}`),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
console.log(
|
|
188
|
+
c.dim(
|
|
189
|
+
`Select Base Sepolia → ETH. CDP ~${budget.dripEth} ETH/claim; claim ~${budget.claimsNeeded} time(s).`,
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
console.log(c.dim("Waiting for balance…"));
|
|
193
|
+
console.log("");
|
|
194
|
+
}
|
|
195
|
+
} else if (!flags.json) {
|
|
196
|
+
console.log(c.dim(`Open faucet: ${budget.faucetUrl}`));
|
|
197
|
+
console.log(c.dim(`Paste address: ${owner}`));
|
|
198
|
+
console.log(c.dim("Waiting for balance…"));
|
|
199
|
+
console.log("");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const deadline = now() + flags.timeoutMs;
|
|
203
|
+
while (now() < deadline) {
|
|
204
|
+
await sleep(flags.pollMs);
|
|
205
|
+
budget = await assess();
|
|
206
|
+
if (!flags.json) {
|
|
207
|
+
console.log(
|
|
208
|
+
c.dim(
|
|
209
|
+
`have ${formatEthTrim(budget.balanceWei)} / need ${formatEthTrim(budget.neededWei)} ETH`,
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (budget.funded) {
|
|
214
|
+
if (flags.json) {
|
|
215
|
+
console.log(
|
|
216
|
+
JSON.stringify({ ok: true, ...budgetToJson(budget) }, null, 2),
|
|
217
|
+
);
|
|
218
|
+
} else {
|
|
219
|
+
console.log("");
|
|
220
|
+
console.log(c.green("Funded. Ready to mint."));
|
|
221
|
+
nextHint([
|
|
222
|
+
"clanker doctor",
|
|
223
|
+
`clanker operator mint ${operator.label ?? "<label>"} --yes`,
|
|
224
|
+
"clanker bot mint <bot_label> --yes",
|
|
225
|
+
]);
|
|
226
|
+
}
|
|
227
|
+
return { exitCode: 0, funded: true, budget };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (flags.json) {
|
|
232
|
+
console.log(
|
|
233
|
+
JSON.stringify(
|
|
234
|
+
{ ok: false, funded: false, timedOut: true, ...budgetToJson(budget) },
|
|
235
|
+
null,
|
|
236
|
+
2,
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
} else {
|
|
240
|
+
console.log("");
|
|
241
|
+
console.log(c.red("Timed out waiting for funds."));
|
|
242
|
+
console.log(formatBudgetSummary(budget));
|
|
243
|
+
nextHint([
|
|
244
|
+
`Open ${budget.faucetUrl} and claim again`,
|
|
245
|
+
`Backup: ${budget.backupFaucetUrl}`,
|
|
246
|
+
"clanker fund",
|
|
247
|
+
]);
|
|
248
|
+
}
|
|
249
|
+
return { exitCode: 1, funded: false, budget, timedOut: true };
|
|
250
|
+
}
|
package/lib/identity-query.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "viem";
|
|
14
14
|
import { clankerIdentityAbi } from "./clanker-identity-abi.mjs";
|
|
15
15
|
|
|
16
|
-
/**
|
|
16
|
+
/** Public RPC eth_getLogs chunk size — wide ranges are often rejected. */
|
|
17
17
|
export const LOG_CHUNK_BLOCKS = 2000n;
|
|
18
18
|
|
|
19
19
|
export function labelToId(label) {
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mint ETH budget helpers: live registry fees + balance + CDP faucet claim estimate.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { formatEther, parseEther } from "viem";
|
|
6
|
+
import { clankerIdentityAbi } from "./clanker-identity-abi.mjs";
|
|
7
|
+
import { readOperator } from "./identity-query.mjs";
|
|
8
|
+
import { isLocalRpc } from "./profile.mjs";
|
|
9
|
+
import {
|
|
10
|
+
ALCHEMY_BASE_SEPOLIA_FAUCET_URL,
|
|
11
|
+
BASE_SEPOLIA_FAUCET_URL,
|
|
12
|
+
CDP_FAUCET_DRIP_ETH,
|
|
13
|
+
} from "./operator-key.mjs";
|
|
14
|
+
|
|
15
|
+
/** Fixed gas cushion for one operator mint + one bot mint (no live estimateGas). */
|
|
16
|
+
export const MINT_GAS_RESERVE_WEI = parseEther("0.00005");
|
|
17
|
+
|
|
18
|
+
/** Documented CDP Base Sepolia ETH drip per claim (wei). */
|
|
19
|
+
export const CDP_FAUCET_DRIP_WEI = parseEther(CDP_FAUCET_DRIP_ETH);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {bigint} wei
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
export function formatEthTrim(wei) {
|
|
26
|
+
const s = formatEther(wei);
|
|
27
|
+
if (!s.includes(".")) return s;
|
|
28
|
+
const trimmed = s.replace(/\.?0+$/, "");
|
|
29
|
+
return trimmed === "" ? "0" : trimmed;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Ceiling of shortfall / drip (integer claims).
|
|
34
|
+
* @param {bigint} shortfallWei
|
|
35
|
+
* @param {bigint} [dripWei]
|
|
36
|
+
* @returns {number}
|
|
37
|
+
*/
|
|
38
|
+
export function claimsNeeded(shortfallWei, dripWei = CDP_FAUCET_DRIP_WEI) {
|
|
39
|
+
if (shortfallWei <= 0n) return 0;
|
|
40
|
+
if (dripWei <= 0n) return 0;
|
|
41
|
+
return Number((shortfallWei + dripWei - 1n) / dripWei);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Pure budget math given fees, balance, and which fees remain.
|
|
46
|
+
*
|
|
47
|
+
* @param {{
|
|
48
|
+
* operatorFee: bigint,
|
|
49
|
+
* botFee: bigint,
|
|
50
|
+
* balance: bigint,
|
|
51
|
+
* needOperatorFee: boolean,
|
|
52
|
+
* needBotFee: boolean,
|
|
53
|
+
* gasReserve?: bigint,
|
|
54
|
+
* local?: boolean,
|
|
55
|
+
* }} opts
|
|
56
|
+
*/
|
|
57
|
+
export function computeMintBudget(opts) {
|
|
58
|
+
const gasReserve = opts.gasReserve ?? MINT_GAS_RESERVE_WEI;
|
|
59
|
+
if (opts.local) {
|
|
60
|
+
return {
|
|
61
|
+
local: true,
|
|
62
|
+
funded: true,
|
|
63
|
+
balanceWei: opts.balance,
|
|
64
|
+
operatorFeeWei: opts.operatorFee,
|
|
65
|
+
botFeeWei: opts.botFee,
|
|
66
|
+
gasReserveWei: gasReserve,
|
|
67
|
+
feesWei: 0n,
|
|
68
|
+
neededWei: 0n,
|
|
69
|
+
shortfallWei: 0n,
|
|
70
|
+
claimsNeeded: 0,
|
|
71
|
+
needOperatorFee: false,
|
|
72
|
+
needBotFee: false,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let feesWei = 0n;
|
|
77
|
+
if (opts.needOperatorFee) feesWei += opts.operatorFee;
|
|
78
|
+
if (opts.needBotFee) feesWei += opts.botFee;
|
|
79
|
+
// If nothing left to mint, needed is 0 (already funded for first operator+bot path).
|
|
80
|
+
const effectiveNeeded =
|
|
81
|
+
!opts.needOperatorFee && !opts.needBotFee ? 0n : feesWei + gasReserve;
|
|
82
|
+
const shortfallWei =
|
|
83
|
+
opts.balance >= effectiveNeeded ? 0n : effectiveNeeded - opts.balance;
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
local: false,
|
|
87
|
+
funded: shortfallWei === 0n,
|
|
88
|
+
balanceWei: opts.balance,
|
|
89
|
+
operatorFeeWei: opts.operatorFee,
|
|
90
|
+
botFeeWei: opts.botFee,
|
|
91
|
+
gasReserveWei: gasReserve,
|
|
92
|
+
feesWei,
|
|
93
|
+
neededWei: effectiveNeeded,
|
|
94
|
+
shortfallWei,
|
|
95
|
+
claimsNeeded: claimsNeeded(shortfallWei),
|
|
96
|
+
needOperatorFee: opts.needOperatorFee,
|
|
97
|
+
needBotFee: opts.needBotFee,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Read live fees + balance and decide remaining fees for the onboarding path.
|
|
103
|
+
*
|
|
104
|
+
* @param {{
|
|
105
|
+
* pub: { readContract: Function, getBalance: Function },
|
|
106
|
+
* registry: string,
|
|
107
|
+
* owner: string,
|
|
108
|
+
* rpc: string,
|
|
109
|
+
* operatorLabel?: string|null,
|
|
110
|
+
* mode?: 'onboarding'|'operator'|'bot',
|
|
111
|
+
* gasReserve?: bigint,
|
|
112
|
+
* }} opts
|
|
113
|
+
*/
|
|
114
|
+
export async function assessMintBudget(opts) {
|
|
115
|
+
const local = isLocalRpc(opts.rpc);
|
|
116
|
+
if (local) {
|
|
117
|
+
return computeMintBudget({
|
|
118
|
+
operatorFee: 0n,
|
|
119
|
+
botFee: 0n,
|
|
120
|
+
balance: 0n,
|
|
121
|
+
needOperatorFee: false,
|
|
122
|
+
needBotFee: false,
|
|
123
|
+
local: true,
|
|
124
|
+
gasReserve: opts.gasReserve,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!opts.registry || !/^0x[0-9a-fA-F]{40}$/.test(opts.registry)) {
|
|
129
|
+
throw new Error("registry address required to assess mint budget");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const [operatorFee, botFee, balance] = await Promise.all([
|
|
133
|
+
opts.pub.readContract({
|
|
134
|
+
address: opts.registry,
|
|
135
|
+
abi: clankerIdentityAbi,
|
|
136
|
+
functionName: "operatorFee",
|
|
137
|
+
}),
|
|
138
|
+
opts.pub.readContract({
|
|
139
|
+
address: opts.registry,
|
|
140
|
+
abi: clankerIdentityAbi,
|
|
141
|
+
functionName: "botFee",
|
|
142
|
+
}),
|
|
143
|
+
opts.pub.getBalance({ address: opts.owner }),
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
const mode = opts.mode ?? "onboarding";
|
|
147
|
+
let needOperatorFee = false;
|
|
148
|
+
let needBotFee = false;
|
|
149
|
+
|
|
150
|
+
if (mode === "operator") {
|
|
151
|
+
needOperatorFee = true;
|
|
152
|
+
needBotFee = false;
|
|
153
|
+
} else if (mode === "bot") {
|
|
154
|
+
needOperatorFee = false;
|
|
155
|
+
needBotFee = true;
|
|
156
|
+
} else {
|
|
157
|
+
// onboarding: operator + first bot unless operator already active
|
|
158
|
+
needBotFee = true;
|
|
159
|
+
needOperatorFee = true;
|
|
160
|
+
if (opts.operatorLabel) {
|
|
161
|
+
try {
|
|
162
|
+
const op = await readOperator(opts.pub, opts.registry, opts.operatorLabel);
|
|
163
|
+
if (op.active) {
|
|
164
|
+
needOperatorFee = false;
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
// keep both fees if read fails
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const budget = computeMintBudget({
|
|
173
|
+
operatorFee: BigInt(operatorFee),
|
|
174
|
+
botFee: BigInt(botFee),
|
|
175
|
+
balance: BigInt(balance),
|
|
176
|
+
needOperatorFee,
|
|
177
|
+
needBotFee,
|
|
178
|
+
gasReserve: opts.gasReserve,
|
|
179
|
+
local: false,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
...budget,
|
|
184
|
+
owner: opts.owner,
|
|
185
|
+
registry: opts.registry,
|
|
186
|
+
operatorLabel: opts.operatorLabel ?? null,
|
|
187
|
+
faucetUrl: BASE_SEPOLIA_FAUCET_URL,
|
|
188
|
+
backupFaucetUrl: ALCHEMY_BASE_SEPOLIA_FAUCET_URL,
|
|
189
|
+
dripEth: CDP_FAUCET_DRIP_ETH,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Human-readable one-liner for doctor / fund.
|
|
195
|
+
* @param {ReturnType<typeof computeMintBudget> & { dripEth?: string }} budget
|
|
196
|
+
*/
|
|
197
|
+
export function formatBudgetSummary(budget) {
|
|
198
|
+
if (budget.local) {
|
|
199
|
+
return "local RPC — Anvil accounts are prefunded";
|
|
200
|
+
}
|
|
201
|
+
if (budget.funded) {
|
|
202
|
+
return `have ${formatEthTrim(budget.balanceWei)} ETH (need ${formatEthTrim(budget.neededWei)} ETH)`;
|
|
203
|
+
}
|
|
204
|
+
const parts = [];
|
|
205
|
+
if (budget.needOperatorFee) parts.push(`operator fee ${formatEthTrim(budget.operatorFeeWei)}`);
|
|
206
|
+
if (budget.needBotFee) parts.push(`bot fee ${formatEthTrim(budget.botFeeWei)}`);
|
|
207
|
+
parts.push(`gas ~${formatEthTrim(budget.gasReserveWei)}`);
|
|
208
|
+
const drip = budget.dripEth ?? CDP_FAUCET_DRIP_ETH;
|
|
209
|
+
return (
|
|
210
|
+
`have ${formatEthTrim(budget.balanceWei)} ETH, need ${formatEthTrim(budget.neededWei)} ETH` +
|
|
211
|
+
` (${parts.join(" + ")}). CDP ~${drip} ETH/claim → claim ~${budget.claimsNeeded} time(s)`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* JSON-serializable budget fields (wei as strings).
|
|
217
|
+
* @param {object} budget
|
|
218
|
+
*/
|
|
219
|
+
export function budgetToJson(budget) {
|
|
220
|
+
return {
|
|
221
|
+
local: Boolean(budget.local),
|
|
222
|
+
funded: Boolean(budget.funded),
|
|
223
|
+
balanceWei: String(budget.balanceWei ?? 0n),
|
|
224
|
+
neededWei: String(budget.neededWei ?? 0n),
|
|
225
|
+
shortfallWei: String(budget.shortfallWei ?? 0n),
|
|
226
|
+
claimsNeeded: budget.claimsNeeded ?? 0,
|
|
227
|
+
operatorFeeWei: String(budget.operatorFeeWei ?? 0n),
|
|
228
|
+
botFeeWei: String(budget.botFeeWei ?? 0n),
|
|
229
|
+
gasReserveWei: String(budget.gasReserveWei ?? 0n),
|
|
230
|
+
needOperatorFee: Boolean(budget.needOperatorFee),
|
|
231
|
+
needBotFee: Boolean(budget.needBotFee),
|
|
232
|
+
owner: budget.owner ?? null,
|
|
233
|
+
faucetUrl: budget.faucetUrl ?? BASE_SEPOLIA_FAUCET_URL,
|
|
234
|
+
backupFaucetUrl: budget.backupFaucetUrl ?? ALCHEMY_BASE_SEPOLIA_FAUCET_URL,
|
|
235
|
+
dripEth: budget.dripEth ?? CDP_FAUCET_DRIP_ETH,
|
|
236
|
+
};
|
|
237
|
+
}
|
package/lib/openclaw-wire.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { join } from "node:path";
|
|
|
8
8
|
import { harnessSnippet } from "./profile.mjs";
|
|
9
9
|
|
|
10
10
|
/** Current published OpenClaw plugin pins for closed-beta invite. */
|
|
11
|
-
export const OPENCLAW_PLUGIN_PIN = "2026.
|
|
11
|
+
export const OPENCLAW_PLUGIN_PIN = "2026.9.10";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* @param {string} [openclawHome]
|
|
@@ -94,11 +94,52 @@ export function wireOpenClawMqtt(opts) {
|
|
|
94
94
|
export function hubConnectChecklist(opts) {
|
|
95
95
|
const pin = opts.pluginPin ?? OPENCLAW_PLUGIN_PIN;
|
|
96
96
|
return [
|
|
97
|
+
"Your bot login is already wired — install plugins and restart OpenClaw.",
|
|
97
98
|
`openclaw plugins install @clanker-chain/mqtt-channel-plugin@${pin}`,
|
|
98
99
|
`openclaw plugins install @clanker-chain/mqtt-tools@${pin}`,
|
|
99
100
|
"Enable plugin ids mqtt + mqtt-tools (already set in openclaw.json if we wired it)",
|
|
100
101
|
`CONNECT broker: ${opts.channelsMqtt?.brokerUrl ?? "mqtts://mqtt.clanker-chain.com:8883"}`,
|
|
101
|
-
|
|
102
|
+
"Both operators: clanker pair add <peer-operator> (Policy — required before DMs deliver)",
|
|
102
103
|
"DM openclaw.france.prod-1 to smoke the mesh",
|
|
103
104
|
];
|
|
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,58 @@
|
|
|
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
|
+
/** Backup Base Sepolia faucet (amounts not guaranteed). */
|
|
18
|
+
export const ALCHEMY_BASE_SEPOLIA_FAUCET_URL =
|
|
19
|
+
"https://www.alchemy.com/faucets/base-sepolia";
|
|
20
|
+
|
|
21
|
+
/** Documented CDP ETH drip per claim on Base Sepolia. */
|
|
22
|
+
export const CDP_FAUCET_DRIP_ETH = "0.0001";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate a new secp256k1 key and write it to destPath (mode 0o600).
|
|
26
|
+
* Does not print the key.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} destPath
|
|
29
|
+
* @param {{ force?: boolean }} [opts]
|
|
30
|
+
* @returns {{ path: string, address: string }}
|
|
31
|
+
*/
|
|
32
|
+
export function generateOperatorKeyFile(destPath, opts = {}) {
|
|
33
|
+
if (existsSync(destPath) && !opts.force) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Key file already exists: ${destPath} (pass --force to overwrite)`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const key = generatePrivateKey();
|
|
39
|
+
const address = privateKeyToAccount(key).address;
|
|
40
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
41
|
+
writeFileSync(destPath, `${key}\n`, { mode: 0o600 });
|
|
42
|
+
return { path: destPath, address };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Plain-language next steps after creating an operator key on Sepolia.
|
|
47
|
+
* @param {{ address: string, label?: string }} opts
|
|
48
|
+
*/
|
|
49
|
+
export function consumerFundHints(opts) {
|
|
50
|
+
const mintLabel = opts.label ? ` ${opts.label}` : "";
|
|
51
|
+
return [
|
|
52
|
+
`Your operator address is ${opts.address} — fund it with Base Sepolia ETH`,
|
|
53
|
+
"clanker fund",
|
|
54
|
+
"clanker doctor",
|
|
55
|
+
`clanker operator mint${mintLabel} --yes`,
|
|
56
|
+
"clanker bot mint <bot_label> --yes",
|
|
57
|
+
];
|
|
58
|
+
}
|