@clanker-chain/clanker-cli 2026.9.10 → 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 +114 -10
- package/lib/clanker-identity-abi.mjs +18 -0
- package/lib/doctor.mjs +91 -4
- package/lib/fund.mjs +250 -0
- package/lib/mint-budget.mjs +237 -0
- package/lib/operator-key.mjs +9 -1
- package/lib/setup.mjs +1 -2
- package/package.json +5 -2
package/bin/clanker.mjs
CHANGED
|
@@ -26,6 +26,11 @@ import {
|
|
|
26
26
|
import { resolveForRead, resolveOperatorKey, resolveReadIdentity } from "../lib/resolve.mjs";
|
|
27
27
|
import { runSetup } from "../lib/setup.mjs";
|
|
28
28
|
import { runDoctor } from "../lib/doctor.mjs";
|
|
29
|
+
import { runFund } from "../lib/fund.mjs";
|
|
30
|
+
import {
|
|
31
|
+
assessMintBudget,
|
|
32
|
+
formatEthTrim,
|
|
33
|
+
} from "../lib/mint-budget.mjs";
|
|
29
34
|
import {
|
|
30
35
|
botIdentityCard,
|
|
31
36
|
botIdentityJson,
|
|
@@ -166,6 +171,7 @@ function usage() {
|
|
|
166
171
|
|
|
167
172
|
Usage:
|
|
168
173
|
clanker setup [--preset sepolia|local] [--operator <label>] [--address 0x…] [--key-file path] [--force]
|
|
174
|
+
clanker fund [--no-open] [--timeout ms] [--json]
|
|
169
175
|
clanker doctor [--json]
|
|
170
176
|
clanker init --preset sepolia|local [--force]
|
|
171
177
|
clanker whoami [--json] [--operator <label>] [--address 0x…] [--with-bots]
|
|
@@ -191,12 +197,12 @@ Profile:
|
|
|
191
197
|
~/.clanker/operator.json label + owner + optional key pointer (never raw hex)
|
|
192
198
|
~/.clanker/keys/ bot keys (also written to ~/.openclaw/keys/)
|
|
193
199
|
|
|
194
|
-
Humans: \`clanker setup\` then \`clanker doctor\` / \`whoami\`.
|
|
200
|
+
Humans: \`clanker setup\` then \`clanker fund\` / \`doctor\` / \`whoami\`.
|
|
195
201
|
Pairing (Policy): both operators run \`clanker pair add\` before DMs deliver on the hub.
|
|
196
202
|
Mutates print a plan and confirm unless --yes or --json.
|
|
197
203
|
whoami is fast by default; pass --with-bots to enrich child bots (or use \`clanker bots\`).
|
|
198
204
|
|
|
199
|
-
See docs/operator-cli.md and docs/trust-model.md.
|
|
205
|
+
See docs/operator-cli.md, docs/prerequisites.md, and docs/trust-model.md.
|
|
200
206
|
`);
|
|
201
207
|
}
|
|
202
208
|
|
|
@@ -410,6 +416,7 @@ async function main() {
|
|
|
410
416
|
because: "setup could not write a valid local profile",
|
|
411
417
|
try: [
|
|
412
418
|
"clanker setup --preset sepolia --operator org.you --address 0x… --yes --force",
|
|
419
|
+
"clanker fund",
|
|
413
420
|
"clanker doctor",
|
|
414
421
|
],
|
|
415
422
|
});
|
|
@@ -417,6 +424,22 @@ async function main() {
|
|
|
417
424
|
return;
|
|
418
425
|
}
|
|
419
426
|
|
|
427
|
+
if (cmd === "fund") {
|
|
428
|
+
try {
|
|
429
|
+
const { exitCode } = await runFund(rest);
|
|
430
|
+
process.exit(exitCode);
|
|
431
|
+
} catch (err) {
|
|
432
|
+
exitCliError({
|
|
433
|
+
error: err.message,
|
|
434
|
+
because: "fund needs a Sepolia profile with an owner address",
|
|
435
|
+
try: [
|
|
436
|
+
"clanker setup --preset sepolia --operator org.you --generate-key --yes --force",
|
|
437
|
+
"clanker fund",
|
|
438
|
+
],
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
420
443
|
if (cmd === "doctor") {
|
|
421
444
|
const { exitCode } = await runDoctor(rest);
|
|
422
445
|
process.exit(exitCode);
|
|
@@ -556,8 +579,9 @@ async function main() {
|
|
|
556
579
|
process.exit(1);
|
|
557
580
|
}
|
|
558
581
|
let planRows;
|
|
582
|
+
let preview;
|
|
559
583
|
try {
|
|
560
|
-
|
|
584
|
+
preview = resolveOperatorKey(flags);
|
|
561
585
|
planRows = [
|
|
562
586
|
["action", "registerOperator"],
|
|
563
587
|
["label", label],
|
|
@@ -573,10 +597,42 @@ async function main() {
|
|
|
573
597
|
try: [
|
|
574
598
|
"export OPERATOR_PRIVATE_KEY=0x…",
|
|
575
599
|
"clanker operator mint " + label + " --key-file ~/.clanker/op.key --yes",
|
|
600
|
+
"clanker fund",
|
|
576
601
|
"clanker doctor",
|
|
577
602
|
],
|
|
578
603
|
});
|
|
579
604
|
}
|
|
605
|
+
try {
|
|
606
|
+
if (preview.network.registry) {
|
|
607
|
+
const pub = await publicClientFromRpc(preview.network.rpc);
|
|
608
|
+
const budget = await assessMintBudget({
|
|
609
|
+
pub,
|
|
610
|
+
registry: preview.network.registry,
|
|
611
|
+
owner: preview.address,
|
|
612
|
+
rpc: preview.network.rpc,
|
|
613
|
+
operatorLabel: label,
|
|
614
|
+
mode: "operator",
|
|
615
|
+
});
|
|
616
|
+
planRows.push(
|
|
617
|
+
["fee", `${formatEthTrim(budget.operatorFeeWei)} ETH`],
|
|
618
|
+
["balance", `${formatEthTrim(budget.balanceWei)} ETH`],
|
|
619
|
+
[
|
|
620
|
+
"needed",
|
|
621
|
+
`${formatEthTrim(budget.neededWei)} ETH (fee + gas cushion)`,
|
|
622
|
+
],
|
|
623
|
+
);
|
|
624
|
+
if (!budget.funded) {
|
|
625
|
+
exitCliError({
|
|
626
|
+
error: `insufficient ETH: have ${formatEthTrim(budget.balanceWei)}, need ${formatEthTrim(budget.neededWei)}`,
|
|
627
|
+
because: "mint sends the exact registry fee plus gas",
|
|
628
|
+
try: ["clanker fund", "clanker doctor"],
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
} catch (err) {
|
|
633
|
+
// Non-fatal if RPC read fails before confirm; mint will still fail clearly.
|
|
634
|
+
planRows.push(["balance", `(could not check: ${err.message ?? err})`]);
|
|
635
|
+
}
|
|
580
636
|
const ok = await confirmPlan(flags, planRows, `Mint operator ${label}?`);
|
|
581
637
|
if (!ok) process.exit(0);
|
|
582
638
|
const result = await chainMintOperator(label, flags);
|
|
@@ -677,10 +733,11 @@ async function main() {
|
|
|
677
733
|
process.exit(1);
|
|
678
734
|
}
|
|
679
735
|
let operatorLabel = operatorArg ?? flagValue(flags, "--operator") ?? null;
|
|
736
|
+
let preview = null;
|
|
680
737
|
if (!operatorLabel) {
|
|
681
738
|
try {
|
|
682
|
-
|
|
683
|
-
const inferred = await resolveOperatorLabel(flags, address, network);
|
|
739
|
+
preview = resolveOperatorKey(flags);
|
|
740
|
+
const inferred = await resolveOperatorLabel(flags, preview.address, preview.network);
|
|
684
741
|
operatorLabel = inferred.label;
|
|
685
742
|
} catch (err) {
|
|
686
743
|
exitCliError({
|
|
@@ -693,13 +750,60 @@ async function main() {
|
|
|
693
750
|
});
|
|
694
751
|
}
|
|
695
752
|
}
|
|
753
|
+
if (!preview) {
|
|
754
|
+
try {
|
|
755
|
+
preview = resolveOperatorKey(flags);
|
|
756
|
+
} catch (err) {
|
|
757
|
+
exitCliError({
|
|
758
|
+
error: err.message,
|
|
759
|
+
because: "bot mint needs a signing key on this network",
|
|
760
|
+
try: [
|
|
761
|
+
`clanker bot mint ${botLabel} ${operatorLabel} --key-file ~/.clanker/op.key --yes`,
|
|
762
|
+
"clanker fund",
|
|
763
|
+
],
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
const planRows = [
|
|
768
|
+
["action", "registerBot"],
|
|
769
|
+
["bot", botLabel],
|
|
770
|
+
["operator", operatorLabel],
|
|
771
|
+
["owner", preview.address],
|
|
772
|
+
["registry", preview.network.registry ?? "(none)"],
|
|
773
|
+
];
|
|
774
|
+
try {
|
|
775
|
+
if (preview.network.registry) {
|
|
776
|
+
const pub = await publicClientFromRpc(preview.network.rpc);
|
|
777
|
+
const budget = await assessMintBudget({
|
|
778
|
+
pub,
|
|
779
|
+
registry: preview.network.registry,
|
|
780
|
+
owner: preview.address,
|
|
781
|
+
rpc: preview.network.rpc,
|
|
782
|
+
operatorLabel,
|
|
783
|
+
mode: "bot",
|
|
784
|
+
});
|
|
785
|
+
planRows.push(
|
|
786
|
+
["fee", `${formatEthTrim(budget.botFeeWei)} ETH`],
|
|
787
|
+
["balance", `${formatEthTrim(budget.balanceWei)} ETH`],
|
|
788
|
+
[
|
|
789
|
+
"needed",
|
|
790
|
+
`${formatEthTrim(budget.neededWei)} ETH (fee + gas cushion)`,
|
|
791
|
+
],
|
|
792
|
+
);
|
|
793
|
+
if (!budget.funded) {
|
|
794
|
+
exitCliError({
|
|
795
|
+
error: `insufficient ETH: have ${formatEthTrim(budget.balanceWei)}, need ${formatEthTrim(budget.neededWei)}`,
|
|
796
|
+
because: "mint sends the exact registry fee plus gas",
|
|
797
|
+
try: ["clanker fund", "clanker doctor"],
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
} catch (err) {
|
|
802
|
+
planRows.push(["balance", `(could not check: ${err.message ?? err})`]);
|
|
803
|
+
}
|
|
696
804
|
const ok = await confirmPlan(
|
|
697
805
|
flags,
|
|
698
|
-
|
|
699
|
-
["action", "registerBot"],
|
|
700
|
-
["bot", botLabel],
|
|
701
|
-
["operator", operatorLabel],
|
|
702
|
-
],
|
|
806
|
+
planRows,
|
|
703
807
|
`Mint bot ${botLabel} under ${operatorLabel}?`,
|
|
704
808
|
);
|
|
705
809
|
if (!ok) process.exit(0);
|
|
@@ -19,6 +19,11 @@ export const clankerIdentityAbi = [
|
|
|
19
19
|
"name": "_feeRecipient",
|
|
20
20
|
"type": "address",
|
|
21
21
|
"internalType": "address"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "_priorRegistry",
|
|
25
|
+
"type": "address",
|
|
26
|
+
"internalType": "address"
|
|
22
27
|
}
|
|
23
28
|
],
|
|
24
29
|
"stateMutability": "nonpayable"
|
|
@@ -176,6 +181,19 @@ export const clankerIdentityAbi = [
|
|
|
176
181
|
],
|
|
177
182
|
"stateMutability": "view"
|
|
178
183
|
},
|
|
184
|
+
{
|
|
185
|
+
"type": "function",
|
|
186
|
+
"name": "priorRegistry",
|
|
187
|
+
"inputs": [],
|
|
188
|
+
"outputs": [
|
|
189
|
+
{
|
|
190
|
+
"name": "",
|
|
191
|
+
"type": "address",
|
|
192
|
+
"internalType": "address"
|
|
193
|
+
}
|
|
194
|
+
],
|
|
195
|
+
"stateMutability": "view"
|
|
196
|
+
},
|
|
179
197
|
{
|
|
180
198
|
"type": "function",
|
|
181
199
|
"name": "proposeOperatorTransfer",
|
package/lib/doctor.mjs
CHANGED
|
@@ -11,10 +11,25 @@ import {
|
|
|
11
11
|
clankerHome,
|
|
12
12
|
} from "./profile.mjs";
|
|
13
13
|
import { detectSetupHints, formatSetupDetectTable } from "./setup-detect.mjs";
|
|
14
|
+
import { publicClientFromRpc } from "./identity-query.mjs";
|
|
15
|
+
import {
|
|
16
|
+
assessMintBudget,
|
|
17
|
+
budgetToJson,
|
|
18
|
+
formatBudgetSummary,
|
|
19
|
+
} from "./mint-budget.mjs";
|
|
14
20
|
import { c, nextHint } from "./ui.mjs";
|
|
15
21
|
|
|
16
22
|
/**
|
|
17
|
-
* @param {{
|
|
23
|
+
* @param {{
|
|
24
|
+
* home?: string,
|
|
25
|
+
* env?: NodeJS.ProcessEnv,
|
|
26
|
+
* openclawDir?: string,
|
|
27
|
+
* castBin?: string,
|
|
28
|
+
* spawn?: Function,
|
|
29
|
+
* fetchImpl?: typeof fetch,
|
|
30
|
+
* publicClient?: { readContract: Function, getBalance: Function },
|
|
31
|
+
* skipBalance?: boolean,
|
|
32
|
+
* }} [opts]
|
|
18
33
|
*/
|
|
19
34
|
export async function runDoctorChecks(opts = {}) {
|
|
20
35
|
const env = opts.env ?? process.env;
|
|
@@ -103,6 +118,59 @@ export async function runDoctorChecks(opts = {}) {
|
|
|
103
118
|
: "Foundry cast not on PATH (optional)",
|
|
104
119
|
});
|
|
105
120
|
|
|
121
|
+
/** @type {object|null} */
|
|
122
|
+
let budget = null;
|
|
123
|
+
|
|
124
|
+
const canCheckBalance =
|
|
125
|
+
!opts.skipBalance &&
|
|
126
|
+
hasOwner &&
|
|
127
|
+
registry &&
|
|
128
|
+
/^0x[0-9a-fA-F]{40}$/.test(registry) &&
|
|
129
|
+
rpc;
|
|
130
|
+
|
|
131
|
+
if (canCheckBalance) {
|
|
132
|
+
try {
|
|
133
|
+
if (isLocalRpc(rpc)) {
|
|
134
|
+
budget = await assessMintBudget({
|
|
135
|
+
pub: { readContract: async () => 0n, getBalance: async () => 0n },
|
|
136
|
+
registry,
|
|
137
|
+
owner: getAddress(operator.owner),
|
|
138
|
+
rpc,
|
|
139
|
+
operatorLabel: operator.label,
|
|
140
|
+
});
|
|
141
|
+
checks.push({
|
|
142
|
+
id: "balance",
|
|
143
|
+
ok: true,
|
|
144
|
+
level: "pass",
|
|
145
|
+
message: formatBudgetSummary(budget),
|
|
146
|
+
});
|
|
147
|
+
} else {
|
|
148
|
+
const pub =
|
|
149
|
+
opts.publicClient ?? (await publicClientFromRpc(rpc));
|
|
150
|
+
budget = await assessMintBudget({
|
|
151
|
+
pub,
|
|
152
|
+
registry,
|
|
153
|
+
owner: getAddress(operator.owner),
|
|
154
|
+
rpc,
|
|
155
|
+
operatorLabel: operator.label,
|
|
156
|
+
});
|
|
157
|
+
checks.push({
|
|
158
|
+
id: "balance",
|
|
159
|
+
ok: budget.funded,
|
|
160
|
+
level: budget.funded ? "pass" : "fail",
|
|
161
|
+
message: formatBudgetSummary(budget),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
} catch (err) {
|
|
165
|
+
checks.push({
|
|
166
|
+
id: "balance",
|
|
167
|
+
ok: false,
|
|
168
|
+
level: "warn",
|
|
169
|
+
message: `balance check skipped: ${err.message ?? err}`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
106
174
|
const authUrl = config?.mqttAuthServiceUrl;
|
|
107
175
|
if (authUrl && typeof fetchImpl === "function") {
|
|
108
176
|
const healthUrl = `${String(authUrl).replace(/\/$/, "")}/health`;
|
|
@@ -146,8 +214,12 @@ export async function runDoctorChecks(opts = {}) {
|
|
|
146
214
|
const readyWhoami = checks
|
|
147
215
|
.filter((ch) => ch.id === "config" || ch.id === "registry" || ch.id === "operator")
|
|
148
216
|
.every((ch) => ch.ok);
|
|
149
|
-
const
|
|
150
|
-
|
|
217
|
+
const balanceOk = !checks.some((ch) => ch.id === "balance" && ch.level === "fail");
|
|
218
|
+
const readyMint =
|
|
219
|
+
readyWhoami &&
|
|
220
|
+
hasKey &&
|
|
221
|
+
!checks.some((ch) => ch.id === "anvil_public" && !ch.ok) &&
|
|
222
|
+
balanceOk;
|
|
151
223
|
|
|
152
224
|
return {
|
|
153
225
|
home,
|
|
@@ -156,17 +228,19 @@ export async function runDoctorChecks(opts = {}) {
|
|
|
156
228
|
readyWhoami,
|
|
157
229
|
readyMint,
|
|
158
230
|
ok: readyWhoami,
|
|
231
|
+
budget,
|
|
159
232
|
};
|
|
160
233
|
}
|
|
161
234
|
|
|
162
235
|
/**
|
|
163
236
|
* @param {string[]} argv
|
|
164
|
-
* @param {{ home?: string, env?: NodeJS.ProcessEnv }} [opts]
|
|
237
|
+
* @param {{ home?: string, env?: NodeJS.ProcessEnv, publicClient?: object, skipBalance?: boolean }} [opts]
|
|
165
238
|
* @returns {Promise<{ exitCode: number, report: object }>}
|
|
166
239
|
*/
|
|
167
240
|
export async function runDoctor(argv = [], opts = {}) {
|
|
168
241
|
const json = argv.includes("--json");
|
|
169
242
|
const report = await runDoctorChecks(opts);
|
|
243
|
+
const budgetJson = report.budget ? budgetToJson(report.budget) : null;
|
|
170
244
|
|
|
171
245
|
if (json) {
|
|
172
246
|
console.log(
|
|
@@ -177,6 +251,15 @@ export async function runDoctor(argv = [], opts = {}) {
|
|
|
177
251
|
readyMint: report.readyMint,
|
|
178
252
|
home: report.home,
|
|
179
253
|
checks: report.checks,
|
|
254
|
+
...(budgetJson
|
|
255
|
+
? {
|
|
256
|
+
balanceWei: budgetJson.balanceWei,
|
|
257
|
+
neededWei: budgetJson.neededWei,
|
|
258
|
+
shortfallWei: budgetJson.shortfallWei,
|
|
259
|
+
claimsNeeded: budgetJson.claimsNeeded,
|
|
260
|
+
budget: budgetJson,
|
|
261
|
+
}
|
|
262
|
+
: {}),
|
|
180
263
|
},
|
|
181
264
|
null,
|
|
182
265
|
2,
|
|
@@ -207,12 +290,16 @@ export async function runDoctor(argv = [], opts = {}) {
|
|
|
207
290
|
}
|
|
208
291
|
if (report.readyMint) {
|
|
209
292
|
console.log(c.green("Ready for: clanker operator mint / bot mint"));
|
|
293
|
+
} else if (report.budget && !report.budget.funded && !report.budget.local) {
|
|
294
|
+
console.log(c.dim("Mint: fund the operator address first (clanker fund)"));
|
|
210
295
|
} else {
|
|
211
296
|
console.log(c.dim("Mint/revoke: need signing key (and non-Anvil owner on public RPC)"));
|
|
212
297
|
}
|
|
213
298
|
|
|
214
299
|
if (!report.readyWhoami) {
|
|
215
300
|
nextHint(["clanker setup"]);
|
|
301
|
+
} else if (report.budget && !report.budget.funded && !report.budget.local) {
|
|
302
|
+
nextHint(["clanker fund", "clanker doctor"]);
|
|
216
303
|
} else if (!report.readyMint) {
|
|
217
304
|
nextHint([
|
|
218
305
|
"clanker whoami",
|
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
|
+
}
|
|
@@ -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/operator-key.mjs
CHANGED
|
@@ -14,6 +14,13 @@ export { defaultOperatorKeyPath };
|
|
|
14
14
|
export const BASE_SEPOLIA_FAUCET_URL =
|
|
15
15
|
"https://portal.cdp.coinbase.com/products/faucet";
|
|
16
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
|
+
|
|
17
24
|
/**
|
|
18
25
|
* Generate a new secp256k1 key and write it to destPath (mode 0o600).
|
|
19
26
|
* Does not print the key.
|
|
@@ -42,7 +49,8 @@ export function generateOperatorKeyFile(destPath, opts = {}) {
|
|
|
42
49
|
export function consumerFundHints(opts) {
|
|
43
50
|
const mintLabel = opts.label ? ` ${opts.label}` : "";
|
|
44
51
|
return [
|
|
45
|
-
`Your operator address is ${opts.address} — fund it with Base Sepolia ETH
|
|
52
|
+
`Your operator address is ${opts.address} — fund it with Base Sepolia ETH`,
|
|
53
|
+
"clanker fund",
|
|
46
54
|
"clanker doctor",
|
|
47
55
|
`clanker operator mint${mintLabel} --yes`,
|
|
48
56
|
"clanker bot mint <bot_label> --yes",
|
package/lib/setup.mjs
CHANGED
|
@@ -34,7 +34,6 @@ import {
|
|
|
34
34
|
resolveFoundryAddress,
|
|
35
35
|
} from "./foundry.mjs";
|
|
36
36
|
import {
|
|
37
|
-
BASE_SEPOLIA_FAUCET_URL,
|
|
38
37
|
consumerFundHints,
|
|
39
38
|
generateOperatorKeyFile,
|
|
40
39
|
} from "./operator-key.mjs";
|
|
@@ -698,7 +697,7 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
698
697
|
nextHint(consumerFundHints({ address, label }));
|
|
699
698
|
} else if (preset === "sepolia") {
|
|
700
699
|
nextHint([
|
|
701
|
-
|
|
700
|
+
"clanker fund",
|
|
702
701
|
"clanker doctor",
|
|
703
702
|
"clanker whoami",
|
|
704
703
|
`clanker bot mint <label>`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clanker-chain/clanker-cli",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.12",
|
|
4
4
|
"description": "CLI for wiring clanker-chain identity and MQTT into OpenClaw and other agentic stacks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@clack/prompts": "^1.8.0",
|
|
27
27
|
"picocolors": "^1.1.1",
|
|
28
|
-
"viem": "^2.
|
|
28
|
+
"viem": "^2.56.3"
|
|
29
|
+
},
|
|
30
|
+
"overrides": {
|
|
31
|
+
"ws": "^8.21.0"
|
|
29
32
|
}
|
|
30
33
|
}
|