brainchain-worker 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,374 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { createRequire } from "node:module";
3
- import { formatEther, parseUnits } from "viem";
4
- import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
5
- import { createChain, faucetReopens, reason, unhealthy } from "./chain.js";
6
- import { claim, createIncome } from "./claim.js";
7
- import { createProofMinter, createProofsChain, MAX_MINT, mint } from "./mint.js";
8
- import { resolveNetwork } from "./network.js";
9
- import { hireable, shortOfStake, snapshot, stake, usdg, wallet, when } from "./standing.js";
10
- import { openStore } from "./state.js";
11
- import { createWork, shellCommand, stopLeftover, stopper } from "./work.js";
12
- const { version } = createRequire(import.meta.url)("../package.json");
13
- const USAGE = `brainchain-worker ${version}
14
-
15
- Earn on BrainChain by doing tasks: stake, apply, and let Jev hire your agent.
16
-
17
- Usage:
18
- brainchain-worker new-key Print a fresh private key and its address.
19
- brainchain-worker join --name <name> --about <text> [--price <USDG>]
20
- Stake the minimum and apply to take tasks. The name is at
21
- most 60 characters, the description at most 500.
22
- brainchain-worker run --exec "<command>" [--format prompt|json] [--timeout <minutes>] [--once] [--no-mint]
23
- Do every task Jev hires you for: the command reads the
24
- task on stdin and prints the deliverable on stdout, within
25
- --timeout minutes (20 by default, at most 1440). The worker
26
- mints the Proof of each task it is paid for, unless --no-mint.
27
- brainchain-worker status Stake, wallet, application, record and earnings.
28
- brainchain-worker price <USDG> Change your asking price per task.
29
- brainchain-worker mint [--count <n>] [--dry]
30
- Mint a Proof for each paid task waiting for one, up to 10 or
31
- --count, at 0.001 WETH each on Robinhood Chain. --dry sends nothing.
32
- brainchain-worker claim [--dry] Claim the ETH due on the worker's Proofs, on Robinhood Chain.
33
- --dry lists what is due and sends nothing.
34
- brainchain-worker leave Start unstaking (3-day cooldown).
35
- brainchain-worker withdraw Withdraw stake once the cooldown has passed.
36
- brainchain-worker --version Print the version.
37
-
38
- Environment:
39
- BRAINCHAIN_WORKER_KEY Private key of the worker's wallet, 64 hex characters with or without 0x.
40
- BRAINCHAIN_NETWORK testnet (the default) or mainnet.
41
- BRAINCHAIN_RPC_URL Overrides the network's RPC.
42
- BRAINCHAIN_SERVICE_URL Overrides the BrainChain service that keeps results: https, or http on
43
- localhost or 127.0.0.1.
44
-
45
- The command sees the worker's environment without any BRAINCHAIN_ variable, plus BRAINCHAIN_TASK_ID.
46
- Unfinished work is kept in ~/.config/brainchain-worker/<address>.<network>.json across restarts,
47
- and one run at a time works for each address on each network.
48
-
49
- Docs: https://docs.usebrainchain.com/run-a-worker`;
50
- /** A mistake in the command line. Every other error is printed after the program's name. */
51
- class UsageError extends Error {
52
- }
53
- /** The flags each command takes; `true` marks a switch, which takes no value. */
54
- const FLAGS = {
55
- join: { name: false, about: false, price: false },
56
- run: { exec: false, format: false, timeout: false, once: true, "no-mint": true },
57
- status: {},
58
- price: {},
59
- leave: {},
60
- withdraw: {},
61
- claim: { dry: true },
62
- mint: { count: false, dry: true },
63
- };
64
- const POSITIONAL = { price: 1 };
65
- function parse(command, args) {
66
- const known = FLAGS[command];
67
- if (known === undefined)
68
- throw new UsageError(`Unknown command "${command}" (see brainchain-worker --help)`);
69
- const positional = [];
70
- const values = new Map();
71
- const switches = new Set();
72
- for (let i = 0; i < args.length; i++) {
73
- const arg = args[i];
74
- if (!arg.startsWith("--")) {
75
- positional.push(arg);
76
- continue;
77
- }
78
- const [name, inline] = arg.slice(2).split(/=(.*)/s, 2);
79
- const isSwitch = known[name];
80
- if (isSwitch === undefined)
81
- throw new UsageError(`The ${command} command has no --${name} flag (see brainchain-worker --help)`);
82
- if (isSwitch) {
83
- switches.add(name);
84
- continue;
85
- }
86
- const value = inline ?? args[++i];
87
- if (value === undefined || (inline === undefined && value.startsWith("--")))
88
- throw new UsageError(`--${name} needs a value`);
89
- values.set(name, value);
90
- }
91
- if (positional.length > (POSITIONAL[command] ?? 0))
92
- throw new UsageError(`Unexpected "${positional.at(-1)}" (see brainchain-worker --help)`);
93
- return { positional, values, switches };
94
- }
95
- function signer() {
96
- const raw = process.env.BRAINCHAIN_WORKER_KEY?.trim();
97
- if (!raw)
98
- throw new Error("Set BRAINCHAIN_WORKER_KEY to the worker's private key. `npx brainchain-worker new-key` makes one.");
99
- const hex = raw.startsWith("0x") ? raw.slice(2) : raw;
100
- if (!/^[0-9a-fA-F]{64}$/.test(hex))
101
- throw new Error("BRAINCHAIN_WORKER_KEY must be 64 hex characters, with or without 0x");
102
- const key = `0x${hex.toLowerCase()}`;
103
- try {
104
- return { key, account: privateKeyToAccount(key) };
105
- }
106
- catch {
107
- throw new Error("BRAINCHAIN_WORKER_KEY is not a valid private key. `npx brainchain-worker new-key` makes one.");
108
- }
109
- }
110
- const DEFAULT_PRICE = "0.20";
111
- /** A price in the bounty token, which has 6 decimals. */
112
- function price(value) {
113
- if (!/^\d{1,6}(\.\d{1,6})?$/.test(value)) {
114
- throw new UsageError(`The price must be a number from 0 to 999999 with at most 6 decimals, such as 0.20, not "${value}"`);
115
- }
116
- return parseUnits(value, 6);
117
- }
118
- const RUN = '`npx brainchain-worker run --exec "<command>"`';
119
- function noGas(chain, network) {
120
- return new Error(`${chain.account} has no ETH on ${network.name} for gas. ${network.gas}; 0.001 ETH is plenty.`);
121
- }
122
- async function needGas(chain, network) {
123
- if ((await chain.ethBalance()) === 0n)
124
- throw noGas(chain, network);
125
- }
126
- async function join(chain, network, values) {
127
- const name = values.get("name")?.trim() ?? "";
128
- const about = values.get("about")?.trim() ?? "";
129
- if (!name)
130
- throw new UsageError("--name is required, up to 60 characters");
131
- if (name.length > 60)
132
- throw new UsageError("--name is at most 60 characters");
133
- if (!about) {
134
- throw new UsageError("--about is required: say what work your agent does in up to 500 characters, since Jev reads it to decide and refuses an empty application");
135
- }
136
- if (about.length > 500)
137
- throw new UsageError("--about is at most 500 characters");
138
- const requested = values.has("price") ? price(values.get("price")) : undefined;
139
- await needGas(chain, network);
140
- const { allowed, w, change, pending, open, applying, toStake } = await snapshot(chain);
141
- // A worker that has staked keeps its price unless --price changes it.
142
- const asking = requested ?? (w.registered ? w.price : price(DEFAULT_PRICE));
143
- const [bond, balance, timelock] = await Promise.all([chain.bond(), chain.stakeBalance(), chain.timelock()]);
144
- const need = toStake + (applying ? bond : 0n);
145
- if (balance < need) {
146
- if (network.buyStake)
147
- throw new Error(shortOfStake(network, w, balance, need, await chain.now()));
148
- const [faucet, now] = await Promise.all([chain.faucet(), chain.now()]);
149
- const closed = (opensAt) => {
150
- const withdraw = now >= w.unlocksAt ? "run withdraw now" : `run withdraw after ${when(w.unlocksAt)}`;
151
- const unstaking = w.unstaking > 0n ? `, or ${withdraw} and join with the ${stake(network, w.unstaking)} it returns` : "";
152
- return new Error(`The wallet holds ${stake(network, balance)} of the ${stake(network, need)} join needs, and the faucet allows one claim a day: ` +
153
- `this wallet can claim again at ${when(opensAt)}. Run join again then${unstaking}.`);
154
- };
155
- if (now < faucet.opensAt)
156
- throw closed(faucet.opensAt);
157
- console.log(`Claiming ${network.stakeSymbol} from the testnet faucet...`);
158
- await chain.claimStakeToken().catch((error) => {
159
- const opensAt = faucetReopens(error);
160
- throw opensAt === undefined ? error : closed(opensAt);
161
- });
162
- }
163
- if (toStake > 0n) {
164
- console.log(`Staking ${stake(network, toStake)}${w.staked > 0n ? " more, up to the minimum," : ""} at ${usdg(network, asking)} per task...`);
165
- await chain.approve(network.contracts.stakeToken, network.contracts.staking, toStake);
166
- await chain.stakeTokens(toStake, asking);
167
- }
168
- else if (w.price !== asking) {
169
- console.log(`Setting your price to ${usdg(network, asking)} per task...`);
170
- await chain.setPrice(asking);
171
- }
172
- if (allowed) {
173
- console.log(`${toStake > 0n ? "Staked back to the minimum, so this worker can be hired again." : "This worker can already be hired."} Start it with ${RUN}.`);
174
- return;
175
- }
176
- if (change && pending) {
177
- console.log(`Jev already allowed this worker. It can be hired after ${when(change.eta)}; keep ${RUN} going until then.`);
178
- return;
179
- }
180
- if (open) {
181
- console.log(`Application ${open.id} is already waiting for Jev.`);
182
- return;
183
- }
184
- console.log(`Applying with a ${stake(network, bond)} bond, returned if Jev allows the worker...`);
185
- await chain.approve(network.contracts.stakeToken, network.contracts.bonds, bond);
186
- const profile = Buffer.from(JSON.stringify({ name, about })).toString("base64");
187
- await chain.apply(`data:application/json;base64,${profile}`);
188
- console.log(`\nApplied. Jev usually decides within a few minutes. If it allows the worker, it can be hired once a ${Number(timelock) / 3600}-hour timelock passes.\n` +
189
- `Start ${RUN} now so it takes its first task as soon as it is hired, and follow\n` +
190
- "the application with `npx brainchain-worker status`. New workers are offered house tasks first, which BrainChain pays for.");
191
- }
192
- async function status(chain, network) {
193
- const [gas, s, held, record, bountyBalance, stakeBalance, bond, faucet, now] = await Promise.all([
194
- chain.ethBalance(),
195
- snapshot(chain),
196
- chain.held(),
197
- chain.record(),
198
- chain.bountyBalance(),
199
- chain.stakeBalance(),
200
- chain.bond(),
201
- // A stake token without a faucet, as on mainnet, leaves out what the faucet would give.
202
- network.buyStake ? undefined : chain.faucet().catch(() => undefined),
203
- chain.now(),
204
- ]);
205
- const { w, min } = s;
206
- const unstaking = w.unstaking > 0n ? `, ${stake(network, w.unstaking)} unstaking until ${when(w.unlocksAt)}` : "";
207
- console.log(`Worker ${chain.account} on ${network.name}
208
- Hireable ${hireable(s, network, "status")}
209
- Stake ${stake(network, w.staked)} (minimum ${stake(network, min)})${unstaking}
210
- Wallet ${wallet(network, s, stakeBalance, bond, faucet, now)}
211
- Price ${w.registered ? `${usdg(network, w.price)} per task` : "not set until you join"}
212
- Record ${record.accepted} accepted, ${record.refunded} refunded, ${record.inFlight} in flight${held > 0 ? " (stake held until they settle)" : ""}
213
- Earned ${usdg(network, record.earned)} after fees; wallet holds ${usdg(network, bountyBalance)}
214
- Gas ${formatEther(gas)} ETH`);
215
- }
216
- async function run(network, { key, account }, values, once, mints) {
217
- const exec = values.get("exec");
218
- if (!exec)
219
- throw new UsageError(`--exec is required: the command that does the work, such as --exec 'claude -p --tools ""'`);
220
- const format = values.get("format") ?? "prompt";
221
- if (format !== "prompt" && format !== "json")
222
- throw new UsageError("--format is prompt or json");
223
- const minutes = values.get("timeout") ?? "20";
224
- if (!/^\d{1,4}(\.\d+)?$/.test(minutes) || Number(minutes) <= 0 || Number(minutes) > 1440) {
225
- throw new UsageError("--timeout is the most minutes the command may spend on one task: more than 0, at most 1440, and 20 by default");
226
- }
227
- const log = (message) => console.log(`${new Date().toISOString().slice(0, 19)}Z ${message}`);
228
- const store = openStore(account.address, network, (message) => log(`warning: ${message}`));
229
- // The lock rules out a live run, so a command still in the state was left by one that died.
230
- await stopLeftover(store, log);
231
- const chain = createChain(network, account, store);
232
- const proofs = createProofMinter({ network, chain: createProofsChain(network.proofs, account), settled: chain.settled, store, log, enabled: mints });
233
- const command = shellCommand(exec);
234
- // An idle stop keeps the exit code of a task that failed earlier in a --once pass.
235
- const stop = stopper({ busy: () => work.busy(), kill: command.kill, log, exit: (code) => process.exit(code || process.exitCode) });
236
- const work = createWork({
237
- chain,
238
- command,
239
- format,
240
- log,
241
- timeoutMs: Number(minutes) * 60_000,
242
- secrets: [key, key.slice(2)],
243
- store,
244
- stopping: stop.stopping,
245
- // A cron job sees a failed task as a failed run.
246
- failed: once ? () => void (process.exitCode = 1) : undefined,
247
- delivered: proofs.track,
248
- });
249
- for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"])
250
- process.on(signal, stop.signal);
251
- let gas;
252
- try {
253
- gas = await chain.ethBalance();
254
- }
255
- catch (error) {
256
- // The rest of a single pass reads the same RPC, so it would only fail the same way twice more.
257
- if (once) {
258
- log(`could not read the chain: ${reason(error)}`);
259
- process.exitCode = 1;
260
- return;
261
- }
262
- log(`could not read the wallet's ETH balance: ${reason(error)}`);
263
- }
264
- if (gas === 0n)
265
- throw noGas(chain, network);
266
- const [line, sick] = await Promise.all([
267
- snapshot(chain).then((s) => hireable(s, network, "run"), (error) => `could not check (${reason(error)})`),
268
- unhealthy(network.service),
269
- ]);
270
- log(`worker ${chain.account} on ${network.name}; hireable: ${line}`);
271
- if (sick)
272
- log(`warning: ${sick}`);
273
- if (once) {
274
- const result = await work.round().catch((error) => {
275
- log(`round failed: ${reason(error)}`);
276
- process.exitCode = 1;
277
- return undefined;
278
- });
279
- if (result?.hired === 0 && result.delivered === 0)
280
- log("no task is hired to this worker right now");
281
- await proofs.round().catch((error) => log(`proofs round failed: ${reason(error)}`));
282
- return;
283
- }
284
- log("waiting for tasks (Ctrl-C to stop)");
285
- // Minting waits on receipts from another chain, so it runs beside the work, one round at a time.
286
- let minting;
287
- while (!stop.stopping()) {
288
- await work.round().catch((error) => log(`round failed: ${reason(error)}`));
289
- minting ??= proofs
290
- .round()
291
- .catch((error) => log(`proofs round failed: ${reason(error)}`))
292
- .finally(() => (minting = undefined));
293
- for (let i = 0; i < 15 && !stop.stopping(); i++)
294
- await new Promise((resolve) => setTimeout(resolve, 1_000));
295
- }
296
- await minting;
297
- log("stopped");
298
- }
299
- async function main(argv) {
300
- const [command, ...rest] = argv;
301
- if (command === undefined || command === "help" || argv.includes("--help") || argv.includes("-h")) {
302
- console.log(USAGE);
303
- return;
304
- }
305
- if (command === "--version" || command === "-v") {
306
- console.log(version);
307
- return;
308
- }
309
- if (command === "new-key") {
310
- const fresh = generatePrivateKey();
311
- console.log(`BRAINCHAIN_WORKER_KEY=${fresh}\naddress ${privateKeyToAccount(fresh).address}\nKeep the key secret; it controls the stake and the earnings.`);
312
- return;
313
- }
314
- const { positional, values, switches } = parse(command, rest);
315
- const network = resolveNetwork(process.env);
316
- if (command === "claim") {
317
- return claim(network, (income) => createIncome(network.proofs, income, signer().account), { dry: switches.has("dry"), log: console.log });
318
- }
319
- if (command === "mint") {
320
- const count = values.get("count");
321
- if (count !== undefined && !(/^\d{1,2}$/.test(count) && Number(count) >= 1 && Number(count) <= MAX_MINT)) {
322
- throw new UsageError(`--count is how many Proofs to mint: 1 to ${MAX_MINT}`);
323
- }
324
- return mint(network, createProofsChain(network.proofs, signer().account), { count: count === undefined ? undefined : Number(count), dry: switches.has("dry"), log: console.log });
325
- }
326
- const worker = signer();
327
- if (command === "run")
328
- return run(network, worker, values, switches.has("once"), !switches.has("no-mint"));
329
- const chain = createChain(network, worker.account);
330
- switch (command) {
331
- case "join":
332
- return join(chain, network, values);
333
- case "status":
334
- return status(chain, network);
335
- case "price": {
336
- if (positional[0] === undefined)
337
- throw new UsageError(`Usage: brainchain-worker price <${network.bountySymbol}>, such as price 0.25`);
338
- const asking = price(positional[0]);
339
- if (!(await chain.stake()).registered)
340
- throw new Error("This worker has not staked yet. Run join, which sets the price too.");
341
- await needGas(chain, network);
342
- await chain.setPrice(asking);
343
- console.log(`Price set to ${usdg(network, asking)} per task.`);
344
- return;
345
- }
346
- case "leave": {
347
- const w = await chain.stake();
348
- if (w.staked === 0n) {
349
- throw new Error(w.unstaking > 0n ? `Nothing left to unstake: ${stake(network, w.unstaking)} is already unstaking until ${when(w.unlocksAt)}.` : "Nothing is staked.");
350
- }
351
- await needGas(chain, network);
352
- await chain.requestUnstake(w.staked);
353
- const after = await chain.stake();
354
- console.log(`Unstaking ${stake(network, w.staked)}. Withdraw with \`npx brainchain-worker withdraw\` after ${when(after.unlocksAt)}, once no task of the worker's is in flight.`);
355
- return;
356
- }
357
- case "withdraw": {
358
- const [w, held, now] = await Promise.all([chain.stake(), chain.held(), chain.now()]);
359
- if (w.unstaking === 0n)
360
- throw new Error("Nothing is unstaking. Run leave first.");
361
- if (now < w.unlocksAt)
362
- throw new Error(`${stake(network, w.unstaking)} unlocks at ${when(w.unlocksAt)}. Withdraw after that.`);
363
- if (held > 0)
364
- throw new Error(`The stake is held until the worker's ${held} task${held === 1 ? "" : "s"} in flight settle.`);
365
- await needGas(chain, network);
366
- await chain.withdraw();
367
- console.log(`Withdrew ${stake(network, w.unstaking)}.`);
368
- return;
369
- }
370
- }
371
- }
2
+ import { reason } from "./chain.js";
3
+ import { main, UsageError } from "./cli.js";
372
4
  main(process.argv.slice(2)).catch((error) => {
373
5
  console.error(error instanceof UsageError ? error.message : `brainchain-worker: ${reason(error)}`);
374
6
  process.exit(1);
package/dist/mint.js CHANGED
@@ -1,6 +1,6 @@
1
- import { createPublicClient, createWalletClient, formatEther, http, parseAbi, parseEventLogs, zeroAddress, } from "viem";
1
+ import { createPublicClient, createWalletClient, formatEther, parseAbi, parseEventLogs, zeroAddress, } from "viem";
2
2
  import { reason, RECEIPT_MS } from "./chain.js";
3
- import { proofsChainOf } from "./network.js";
3
+ import { proofsChainOf, proofsTransport } from "./network.js";
4
4
  import { when } from "./standing.js";
5
5
  export const collectionAbi = parseAbi([
6
6
  "function PRICE_QUOTE() view returns (uint256)",
@@ -38,7 +38,7 @@ export function mintedToken(logs, collection, to) {
38
38
  }
39
39
  export function createProofsChain(proofs, account) {
40
40
  const chain = proofsChainOf(proofs);
41
- const transport = http(proofs.rpc, { timeout: 20_000, retryCount: 3 });
41
+ const transport = proofsTransport(proofs);
42
42
  const client = createPublicClient({ chain, transport, pollingInterval: 1_000 });
43
43
  const wallet = createWalletClient({ account, chain, transport });
44
44
  const weth = { address: proofs.weth, abi: wethAbi };
package/dist/network.js CHANGED
@@ -1,4 +1,4 @@
1
- import { defineChain, zeroAddress } from "viem";
1
+ import { defineChain, fallback, http, zeroAddress } from "viem";
2
2
  /** BrainChain testnet, release 3.1.0. */
3
3
  export const TESTNET = {
4
4
  id: "testnet",
@@ -15,6 +15,7 @@ export const TESTNET = {
15
15
  chainId: 46630,
16
16
  name: "Robinhood Chain testnet",
17
17
  rpc: "https://rpc.testnet.chain.robinhood.com",
18
+ rpcFallbacks: ["https://robinhood-sepolia-rpc.publicnode.com", "https://robinhood-testnet.drpc.org"],
18
19
  collection: "0x324B52EE2182641ec29652076ab541bc9F9907d2",
19
20
  weth: "0x1F852eF3Afa13156b807204d838BbB46B4b452eb",
20
21
  faucet: true,
@@ -25,6 +26,7 @@ export const TESTNET = {
25
26
  staking: "0xbdC08799D865DA79FE44a469de1FeaE84bb45059",
26
27
  bonds: "0x574060D1A7CA8f3786d2D14fe1A782cc4638CB1e",
27
28
  executor: "0x420484B139001ED5B1DA330e689Cc2f4814001Fb",
29
+ registry: "0x297AA5fb95Ef9785Da6DC0cA00121060606e2C63",
28
30
  stakeToken: "0xc717546497343911750768335b649b86d0a6eCcf",
29
31
  bountyToken: "0xC4ec72794bF3Ce9ba1DE4B38085464c2eb0cDCf2",
30
32
  },
@@ -54,6 +56,7 @@ export const MAINNET = {
54
56
  chainId: 4663,
55
57
  name: "Robinhood Chain",
56
58
  rpc: "https://rpc.mainnet.chain.robinhood.com",
59
+ rpcFallbacks: ["https://rpc-robinhood.globalstake.io", "https://robinhood.api.pocket.network"],
57
60
  collection: "0xE6E6B7DBF1a66dFB25c6F6A87a916B3186113169",
58
61
  weth: "0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73",
59
62
  gas: "Send it a little ETH there for gas",
@@ -64,6 +67,7 @@ export const MAINNET = {
64
67
  staking: MAINNET_WORKER_STAKING,
65
68
  bonds: MAINNET_PROPOSAL_BONDS,
66
69
  executor: MAINNET_DECISION_EXECUTOR,
70
+ registry: "0x65B71706bE8a92dda39dDfdb6ABA8CEA64A33865",
67
71
  stakeToken: BRAIN,
68
72
  bountyToken: "0x8eBFEce96c0AeF7Fd23425D846a302a552c8c451",
69
73
  },
@@ -112,7 +116,12 @@ export function proofsChainOf(proofs) {
112
116
  id: proofs.chainId,
113
117
  name: proofs.name,
114
118
  nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
115
- rpcUrls: { default: { http: [proofs.rpc] } },
119
+ rpcUrls: { default: { http: [proofs.rpc, ...(proofs.rpcFallbacks ?? [])] } },
116
120
  contracts: { multicall3: { address: MULTICALL3 } },
117
121
  });
118
122
  }
123
+ /** The Proofs chain's RPCs in order; the next answers when one fails to, a revert being an answer. */
124
+ export function proofsTransport(proofs) {
125
+ const urls = [proofs.rpc, ...(proofs.rpcFallbacks ?? [])];
126
+ return fallback(urls.map((url) => http(url, { timeout: 20_000, retryCount: urls.length > 1 ? 1 : 3 })), { rank: false, retryCount: 0 });
127
+ }