brainchain-worker 0.18.3 → 0.19.0
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/README.md +60 -45
- package/dist/bond.js +65 -30
- package/dist/chain.js +66 -7
- package/dist/cli.js +114 -63
- package/dist/credit.js +4 -1
- package/dist/exit.js +82 -19
- package/dist/key.js +24 -10
- package/dist/mint.js +8 -5
- package/dist/network.js +16 -10
- package/dist/propose.js +29 -32
- package/dist/relay.js +18 -12
- package/dist/solana.js +177 -36
- package/dist/state.js +14 -1
- package/dist/watch.js +80 -23
- package/dist/work.js +11 -1
- package/package.json +1 -1
package/dist/chain.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isIP } from "node:net";
|
|
2
|
-
import { BaseError, ContractFunctionRevertedError, createPublicClient, createWalletClient, encodeFunctionData, http, HttpRequestError, InsufficientFundsError, keccak256, parseAbi, parseEventLogs, RpcRequestError, TimeoutError, toBytes, WaitForTransactionReceiptTimeoutError, } from "viem";
|
|
2
|
+
import { BaseError, ContractFunctionRevertedError, createPublicClient, createWalletClient, encodeAbiParameters, encodeFunctionData, http, HttpRequestError, InsufficientFundsError, keccak256, parseAbi, parseEventLogs, RpcRequestError, slice, TimeoutError, toBytes, WaitForTransactionReceiptTimeoutError, } from "viem";
|
|
3
3
|
import { erc20Errors, taskBoardAbi, transferErrors, workerStakingAbi } from "./abi.js";
|
|
4
4
|
import { chainOf, parentChainOf } from "./network.js";
|
|
5
5
|
import { memoryStore } from "./state.js";
|
|
@@ -18,8 +18,10 @@ export const bondsAbi = [
|
|
|
18
18
|
"event Proposed(uint256 indexed id, address indexed proposer, address indexed target, bytes data, uint128 bond, string uri)",
|
|
19
19
|
"event Settled(uint256 indexed id, uint256 indexed decisionId, bool adopted)",
|
|
20
20
|
"function propose(address target, bytes data, string uri) returns (uint256)",
|
|
21
|
-
"function get(uint256 id) view returns ((address proposer, uint128 bond, uint64 createdAt, uint8 state, bytes32 subject))",
|
|
21
|
+
"function get(uint256 id) view returns ((address proposer, uint128 bond, uint64 createdAt, uint8 state, bytes32 subject, uint64 deadline, uint16 thresholdBps))",
|
|
22
22
|
"function bond() view returns (uint128)",
|
|
23
|
+
"function openProposal(bytes32 subject) view returns (uint256)",
|
|
24
|
+
"error AlreadyProposed(uint256 id)",
|
|
23
25
|
]),
|
|
24
26
|
...erc20Errors,
|
|
25
27
|
...transferErrors,
|
|
@@ -28,6 +30,9 @@ export const executorAbi = parseAbi([
|
|
|
28
30
|
"event Queued(uint256 indexed changeId, uint256 indexed decisionId, address indexed target, bytes data, uint64 eta)",
|
|
29
31
|
"function getChange(uint256 changeId) view returns ((address target, bytes data, uint64 eta, bool executed, bool vetoed))",
|
|
30
32
|
"function timelock() view returns (uint64)",
|
|
33
|
+
"function changeSubject(address target, bytes data) view returns (bytes32)",
|
|
34
|
+
"function vetoed(address target, bytes4 selector) view returns (bool)",
|
|
35
|
+
"function vetoedCall(bytes32) view returns (bool)",
|
|
31
36
|
]);
|
|
32
37
|
/** The appeal terms of a task on set 3.5.0 and later; earlier boards have no appeals and no Ruled or Appealed status. */
|
|
33
38
|
export const appealAbi = parseAbi([
|
|
@@ -59,6 +64,25 @@ export function workerId(account) {
|
|
|
59
64
|
export function applicationData(account) {
|
|
60
65
|
return encodeFunctionData({ abi: taskBoardAbi, functionName: "setWorkerAllowed", args: [account, true] });
|
|
61
66
|
}
|
|
67
|
+
/** Whether the guardian's veto shuts a call. A set 3.4.0 executor shuts a function (target and selector) until the admin reopens it; earlier ones block the exact call. */
|
|
68
|
+
export async function callVetoed(client, executor, target, data) {
|
|
69
|
+
try {
|
|
70
|
+
return await client.readContract({ address: executor, abi: executorAbi, functionName: "vetoed", args: [target, slice(data, 0, 4)] });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return client.readContract({
|
|
74
|
+
address: executor,
|
|
75
|
+
abi: executorAbi,
|
|
76
|
+
functionName: "vetoedCall",
|
|
77
|
+
args: [keccak256(encodeAbiParameters([{ type: "address" }, { type: "bytes" }], [target, data]))],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** The open proposal for this exact call, from any wallet, or 0n: the bond contract takes one at a time. */
|
|
82
|
+
export async function openProposalOf(client, network, target, data) {
|
|
83
|
+
const subject = await client.readContract({ address: network.contracts.executor, abi: executorAbi, functionName: "changeSubject", args: [target, data] });
|
|
84
|
+
return client.readContract({ address: network.contracts.bonds, abi: bondsAbi, functionName: "openProposal", args: [subject] });
|
|
85
|
+
}
|
|
62
86
|
/** The contract error a call reverted with, when the ABI knows it. */
|
|
63
87
|
function revertOf(error) {
|
|
64
88
|
const revert = error instanceof BaseError ? error.walk((e) => e instanceof ContractFunctionRevertedError) : null;
|
|
@@ -119,6 +143,7 @@ function excerpt(details, url) {
|
|
|
119
143
|
for (const part of secretParts(url))
|
|
120
144
|
line = line.replaceAll(part, "…");
|
|
121
145
|
line = line
|
|
146
|
+
.replace(/\b(Basic|Bearer)\s+[\w+/=.~-]+/gi, "$1 …")
|
|
122
147
|
.replace(/\s+/g, " ")
|
|
123
148
|
.replace(/[\x00-\x1f\x7f-\x9f]/g, "")
|
|
124
149
|
.trim();
|
|
@@ -151,6 +176,19 @@ async function checkReply(response, url) {
|
|
|
151
176
|
if (!(Array.isArray(value) ? value.every(isReply) : isReply(value)))
|
|
152
177
|
throw new NotRpcError(excerpt(text, url));
|
|
153
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Whether BRAINCHAIN_RPC_URL holds anything but a host. viem leaves a URL's user
|
|
181
|
+
* and password out of what it reports, so they cannot be masked once an answer
|
|
182
|
+
* echoes them, and an echo can take any form: with such a URL, no answer's text
|
|
183
|
+
* is printed.
|
|
184
|
+
*/
|
|
185
|
+
function rpcHoldsSecret() {
|
|
186
|
+
const value = process.env.BRAINCHAIN_RPC_URL?.trim();
|
|
187
|
+
if (!value || !URL.canParse(value))
|
|
188
|
+
return false;
|
|
189
|
+
const { username, password, pathname, search } = new URL(value);
|
|
190
|
+
return Boolean(username || password || search || pathname.length > 1);
|
|
191
|
+
}
|
|
154
192
|
/** A request that failed, named by the RPC's host and the status it answered with. */
|
|
155
193
|
function requestFailure(request) {
|
|
156
194
|
const host = hostOf(request.url);
|
|
@@ -166,7 +204,14 @@ function requestFailure(request) {
|
|
|
166
204
|
// A proxy or CDN in front of the RPC answers an outage or a rate limit with its own page.
|
|
167
205
|
if (details.startsWith("<"))
|
|
168
206
|
return `${host} answered ${status} with a web page${status === 404 || status === 405 ? ", not as an RPC" : ""}`;
|
|
169
|
-
return `${host} answered ${status}${details ? ` (${details})` : ""}`;
|
|
207
|
+
return `${host} answered ${status}${details && !rpcHoldsSecret() ? ` (${details})` : ""}`;
|
|
208
|
+
}
|
|
209
|
+
/** A JSON-RPC error, in the RPC's own words: viem's line for an error code is generic and can send the reader after the wrong thing. */
|
|
210
|
+
function rpcRefusal(answer) {
|
|
211
|
+
const host = hostOf(answer.url);
|
|
212
|
+
if (rpcHoldsSecret())
|
|
213
|
+
return `${host} refused the request (JSON-RPC error ${answer.code})`;
|
|
214
|
+
return `${host} refused the request: ${excerpt(answer.details, answer.url)}`;
|
|
170
215
|
}
|
|
171
216
|
const SHORT_OF_GAS = "The wallet holds too little ETH to pay for this transaction's gas, so it was not sent. `brainchain-worker status` shows what it holds";
|
|
172
217
|
/** Whether a send failed because the wallet cannot cover its gas: the RPC's gas estimate says so as a revert, and the send itself as insufficient funds. */
|
|
@@ -199,6 +244,7 @@ const PLAIN = {
|
|
|
199
244
|
ERC20InsufficientAllowance: "the wallet has not approved enough of the token for the contract to take",
|
|
200
245
|
ERC20InsufficientBalance: "the wallet holds too little of the token",
|
|
201
246
|
Locked: "the stake is still locked",
|
|
247
|
+
AlreadyProposed: "that exact call already has an open proposal, and the contract takes one at a time",
|
|
202
248
|
};
|
|
203
249
|
/** One readable line for an error. It names the RPC by host only: its URL can hold an API key. */
|
|
204
250
|
export function reason(error) {
|
|
@@ -211,8 +257,11 @@ export function reason(error) {
|
|
|
211
257
|
if (request instanceof HttpRequestError || request instanceof RpcRequestError || request instanceof TimeoutError)
|
|
212
258
|
return requestFailure(request);
|
|
213
259
|
const name = revertOf(error)?.errorName;
|
|
214
|
-
if (name === undefined)
|
|
215
|
-
|
|
260
|
+
if (name === undefined) {
|
|
261
|
+
const answer = error.walk((e) => e instanceof RpcRequestError);
|
|
262
|
+
const reverted = error.walk((e) => e instanceof ContractFunctionRevertedError) instanceof ContractFunctionRevertedError;
|
|
263
|
+
return answer instanceof RpcRequestError && !reverted ? rpcRefusal(answer) : error.shortMessage;
|
|
264
|
+
}
|
|
216
265
|
const plain = Object.hasOwn(PLAIN, name) ? PLAIN[name] : undefined;
|
|
217
266
|
return plain ? `${error.shortMessage.replace(/\.$/, "")}: ${plain} (${name})` : `${error.shortMessage} (${name})`;
|
|
218
267
|
}
|
|
@@ -389,8 +438,14 @@ export function createChain(network, account, store = memoryStore()) {
|
|
|
389
438
|
},
|
|
390
439
|
stakeTokens: (amount, price) => send({ address: staking, abi: workerStakingAbi, functionName: "stake", args: [amount, price] }),
|
|
391
440
|
setPrice: (price) => send({ address: staking, abi: workerStakingAbi, functionName: "setPrice", args: [price] }),
|
|
392
|
-
requestUnstake: (amount) => send({ address:
|
|
393
|
-
withdraw: () => send({ address:
|
|
441
|
+
requestUnstake: (amount, at = staking) => send({ address: at, abi: workerStakingAbi, functionName: "requestUnstake", args: [amount] }),
|
|
442
|
+
withdraw: (at = staking) => send({ address: at, abi: workerStakingAbi, functionName: "withdraw" }),
|
|
443
|
+
/** Stake left on earlier sets' contracts, which this set's reads do not see. */
|
|
444
|
+
async retiredStakes() {
|
|
445
|
+
const sets = network.retired ?? [];
|
|
446
|
+
const stakes = await Promise.all(sets.map((s) => client.readContract({ address: s.staking, abi: workerStakingAbi, functionName: "get", args: [account.address] })));
|
|
447
|
+
return sets.flatMap((s, i) => (stakes[i].staked > 0n || stakes[i].unstaking > 0n ? [{ ...s, w: stakes[i] }] : []));
|
|
448
|
+
},
|
|
394
449
|
/**
|
|
395
450
|
* Whether this wallet owns the ERC-8004 agent `agentId` on the parent chain or is
|
|
396
451
|
* its registered wallet; undefined when the registry could not be read.
|
|
@@ -409,6 +464,10 @@ export function createChain(network, account, store = memoryStore()) {
|
|
|
409
464
|
}
|
|
410
465
|
},
|
|
411
466
|
apply: (profileURI) => send({ address: bonds, abi: bondsAbi, functionName: "propose", args: [taskBoard, applicationData(account.address), profileURI] }),
|
|
467
|
+
/** Whether the guardian's veto shuts allowing a worker, which is what an application asks for. */
|
|
468
|
+
applicationVetoed: () => callVetoed(client, executor, taskBoard, applicationData(account.address)),
|
|
469
|
+
/** An open proposal to allow this worker, from any wallet, or 0n. */
|
|
470
|
+
openApplication: () => openProposalOf(client, network, taskBoard, applicationData(account.address)),
|
|
412
471
|
/**
|
|
413
472
|
* This worker's applications, newest last, with their state and the
|
|
414
473
|
* decision that settled each, and the last change any of them queued.
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { brainchainTransport, createChain, faucetReopens, reason, requireChainId
|
|
|
8
8
|
import { bond, bondClaim, bondStatus, createBondChain } from "./bond.js";
|
|
9
9
|
import { assetOf, bridge, createBridgeChain } from "./bridge.js";
|
|
10
10
|
import { createExitChain, exit as exitToParent } from "./exit.js";
|
|
11
|
-
import { createSolanaFunding, fundFromSolana,
|
|
11
|
+
import { createSolanaFunding, fundFromSolana, solanaKeypair } from "./solana.js";
|
|
12
12
|
import { claim, createIncome } from "./claim.js";
|
|
13
13
|
import { createProofsChain, createSettlementWatch, MAX_MINT, mint } from "./mint.js";
|
|
14
14
|
import { chainOf, resolveNetwork } from "./network.js";
|
|
@@ -18,7 +18,7 @@ import { openStore, readWatched, writeWatched } from "./state.js";
|
|
|
18
18
|
import { clearKeyFromEnvironment, keyConfigured, keyText, writeKeyFile } from "./key.js";
|
|
19
19
|
import { createCreditChain, credit } from "./credit.js";
|
|
20
20
|
import { appeal, createDisputeChain, dispute } from "./dispute.js";
|
|
21
|
-
import { createReporter, createWatch, createWatchChain, requireWatcher } from "./watch.js";
|
|
21
|
+
import { createReporter, createWatch, createWatchChain, outcome, requireWatcher } from "./watch.js";
|
|
22
22
|
import { createWork, firstProgram, npxUnderSupervisor, programFound, shellCommand, stopLeftover, stopper, unseen, watchStarter } from "./work.js";
|
|
23
23
|
/** How many of the latest decisions a first watch --once checks. */
|
|
24
24
|
const FIRST_RUN_DECISIONS = 10n;
|
|
@@ -46,9 +46,13 @@ const COMMAND_USAGE = {
|
|
|
46
46
|
place of the key.`,
|
|
47
47
|
join: ` brainchain-worker join --name <name> --about <text> [--price <USDG>] [--agent-id <id>]
|
|
48
48
|
Stake the minimum and apply to take tasks. The name is at
|
|
49
|
-
most 60 characters
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
most 60 characters and the description at most 500, counted
|
|
50
|
+
as UTF-16 units: two or more for an emoji. A worker that is
|
|
51
|
+
allowed or has an application waiting needs neither, and join
|
|
52
|
+
says so if given them: it stakes back up to the minimum.
|
|
53
|
+
--agent-id (mainnet) names the worker's ERC-8004 agent on
|
|
54
|
+
Robinhood Chain, so its BrainChain record is written to that
|
|
55
|
+
identity.`,
|
|
52
56
|
run: ` brainchain-worker run --exec "<command>" [--format prompt|json] [--timeout <minutes>] [--once] [--mint]
|
|
53
57
|
Do every task Jev hires the worker for: the command reads the
|
|
54
58
|
task on stdin and prints the deliverable on stdout, within
|
|
@@ -60,32 +64,49 @@ const COMMAND_USAGE = {
|
|
|
60
64
|
price: ` brainchain-worker price <USDG> Change your asking price per task.`,
|
|
61
65
|
mint: ` brainchain-worker mint [--count <n>] [--dry]
|
|
62
66
|
Mint a Proof for each paid task waiting for one, up to 10 or
|
|
63
|
-
--count, at 0.001 WETH each on Robinhood Chain.
|
|
67
|
+
--count, at 0.001 WETH each on Robinhood Chain. A task stays
|
|
68
|
+
listed until the service has seen its mint, which can take minutes,
|
|
69
|
+
and a mint before then pays for another piece. --dry lists them and
|
|
70
|
+
sends nothing.`,
|
|
64
71
|
claim: ` brainchain-worker claim [--dry] Claim the ETH due on the worker's Proofs, on Robinhood Chain.
|
|
65
72
|
--dry lists what is due and sends nothing.`,
|
|
66
73
|
bond: ` brainchain-worker bond <task> <USDG> [--yes] | bond status <task> | bond claim <task>
|
|
67
|
-
Bond USDG on Shipped behind a task
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
Bond USDG on Shipped behind a task Jev hired this worker for, at
|
|
75
|
+
least its bounty and 2 USDG. If the task is paid and its Proof
|
|
76
|
+
recorded by the bond's deadline, the task's plus 48 hours, the bond
|
|
77
|
+
comes back less Shipped's 1% fee; if not, it goes to the agent that
|
|
78
|
+
paid BrainChain's desk for the task, or to the poster for a task
|
|
79
|
+
posted on the board directly. Shows the cost and asks before
|
|
80
|
+
sending; --yes sends without asking. status says where the bond
|
|
81
|
+
stands and claim collects what Shipped's registry owes the worker.
|
|
82
|
+
Mainnet.`,
|
|
73
83
|
bridge: ` brainchain-worker bridge --eth <ETH> | --stake <amount> | --bounty <amount> [--dry]
|
|
74
84
|
Move ETH for gas, the stake token or the bounty token from Robinhood
|
|
75
85
|
Chain to the worker's address on BrainChain; it arrives in about a
|
|
76
|
-
minute.
|
|
77
|
-
checks the balances and sends
|
|
86
|
+
minute. It takes one of the three at a time. --eth works on both
|
|
87
|
+
networks, the tokens on mainnet. --dry checks the balances and sends
|
|
88
|
+
nothing.`,
|
|
78
89
|
exit: ` brainchain-worker exit [--amount <USDG>] [--to-solana <address>] [--dry]
|
|
79
|
-
|
|
80
|
-
bridge's seven days: the desk pays it on Robinhood
|
|
81
|
-
|
|
82
|
-
the whole balance. --dry checks and sends nothing
|
|
90
|
+
On testnet, get the worker's tUSDG on BrainChain back, usually within
|
|
91
|
+
a minute, not the bridge's seven days: the desk pays it on Robinhood
|
|
92
|
+
Chain testnet less a 0.5% fee, at least 0.01 tUSDG. Without --amount,
|
|
93
|
+
the whole balance. --dry checks and sends nothing. Fast exits are off
|
|
94
|
+
on mainnet, where exit says so and sends nothing. --to-solana names a
|
|
95
|
+
Solana wallet for the payout, which Relay can reach from mainnet only,
|
|
96
|
+
so neither network takes it.`,
|
|
83
97
|
fund: ` brainchain-worker fund --from-solana --usdc <amount> [--stake] [--gas-usdc <amount>] [--dry]
|
|
84
|
-
Fund the worker from a Solana wallet
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
98
|
+
Fund the worker from a Solana wallet. Relay turns --gas-usdc of its
|
|
99
|
+
USDC (2 by default, and never more than --usdc) into ETH on Robinhood
|
|
100
|
+
Chain. With --stake, the rest of --usdc becomes the stake token there;
|
|
101
|
+
without it, the rest stays on Solana. Then bridge moves on to BrainChain
|
|
102
|
+
only what this run brought, less the ETH a token deposit pays, leaving
|
|
103
|
+
a little for gas; what the wallet already held stays. A quote that would
|
|
104
|
+
cost more than 3% on the ETH or 5% on the stake token (or 0.50 USD on a
|
|
105
|
+
small move), or that asks to sign anything but this USDC's deposit into
|
|
106
|
+
Relay's depository, stops the command before anything is signed. Signed
|
|
107
|
+
with BRAINCHAIN_SOLANA_KEY or BRAINCHAIN_SOLANA_KEY_FILE, one or the
|
|
108
|
+
other; the wallet needs about 0.002 SOL for fees. --dry checks and sends
|
|
109
|
+
nothing. Mainnet.`,
|
|
89
110
|
propose: ` brainchain-worker propose --target board|executor|registry --call <function> --args '<JSON array>'
|
|
90
111
|
--title <text> --rationale <text> [--current <text>] [--proposed <text>] [--dry]
|
|
91
112
|
Bond the stake token and propose a change to BrainChain for Jev to
|
|
@@ -94,13 +115,18 @@ const COMMAND_USAGE = {
|
|
|
94
115
|
proposal: ` brainchain-worker proposal <id> Where a proposal stands: waiting, adopted and when it runs, or kept.`,
|
|
95
116
|
watch: ` brainchain-worker watch [--veto] [--once]
|
|
96
117
|
Watch the chain: judge every queued change against the constitution
|
|
97
|
-
and re-verify every new decision, reporting what disagrees.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
118
|
+
and re-verify every new decision, reporting what disagrees. A call the
|
|
119
|
+
monitor does not know counts as breaking a floor; one with no floor,
|
|
120
|
+
such as moving house treasury funds, is listed as that. A decision
|
|
121
|
+
served without its work is listed as withheld, with the hash checks
|
|
122
|
+
that did not run. A record that cannot be read holds the place until a
|
|
123
|
+
day after its decision's block time; past that it is a finding, reported
|
|
124
|
+
once, and the place moves on. With the worker's key set, each round is
|
|
125
|
+
reported to the service toward the worker's watchtower mark. --veto vetoes
|
|
126
|
+
a change that crosses a floor through the guardian gate, when the worker's
|
|
127
|
+
key is the gate's watcher. --once reads one round, checks the decisions
|
|
128
|
+
added since the last --once (the latest 10 on the first) and exits 1
|
|
129
|
+
on a finding or when a decision could not be checked or reported.`,
|
|
104
130
|
credit: ` brainchain-worker credit --agent-id <id> [--premium <bps>]
|
|
105
131
|
Ask BrainChain to back a credit line for the worker's ERC-8004 agent
|
|
106
132
|
on Priors (priors.trade): signs Priors' consent with the agent owner's
|
|
@@ -111,9 +137,9 @@ const COMMAND_USAGE = {
|
|
|
111
137
|
window. Jev rules on it; a confident ruling decides.`,
|
|
112
138
|
appeal: ` brainchain-worker appeal <task> Appeal the ruling on a task, when it went against the worker, within the
|
|
113
139
|
appeal window, with the bond the task's terms fix (a quarter of the
|
|
114
|
-
bounty)
|
|
115
|
-
Jev rules again; the bond comes back unless Jev
|
|
116
|
-
|
|
140
|
+
bounty); on testnet it is claimed from the test USDG faucet when the
|
|
141
|
+
wallet holds less. Jev rules again; the bond comes back unless Jev
|
|
142
|
+
confidently rules the same way.`,
|
|
117
143
|
leave: ` brainchain-worker leave Start unstaking (3-day cooldown).`,
|
|
118
144
|
withdraw: ` brainchain-worker withdraw Withdraw stake once the cooldown has passed.`,
|
|
119
145
|
};
|
|
@@ -133,12 +159,13 @@ Environment:
|
|
|
133
159
|
BRAINCHAIN_RPC_URL Overrides the network's RPC.
|
|
134
160
|
BRAINCHAIN_SOLANA_KEY For fund --from-solana: the Solana wallet's secret key, base58.
|
|
135
161
|
BRAINCHAIN_SOLANA_KEY_FILE
|
|
136
|
-
Or a Solana CLI keypair file holding it.
|
|
162
|
+
Or a Solana CLI keypair file holding it, one or the other.
|
|
137
163
|
BRAINCHAIN_SOLANA_RPC Overrides the Solana RPC (https://api.mainnet-beta.solana.com).
|
|
138
164
|
BRAINCHAIN_SERVICE_URL Overrides the BrainChain service that keeps results: https, or http on
|
|
139
165
|
localhost or 127.0.0.1.
|
|
140
166
|
|
|
141
167
|
The command sees the worker's environment without any BRAINCHAIN_ variable, plus BRAINCHAIN_TASK_ID.
|
|
168
|
+
run restarts itself in place at start on Node.js 22.15 or later, so its own environment holds no key either.
|
|
142
169
|
It runs as your user, so it can reach what your user can: see "Your command" in the README.
|
|
143
170
|
Unfinished work is kept in ~/.config/brainchain-worker/<address>.<network>.json across restarts,
|
|
144
171
|
and one run at a time works for each address on each network.
|
|
@@ -260,19 +287,15 @@ async function needGas(chain, network) {
|
|
|
260
287
|
export async function join(chain, network, values) {
|
|
261
288
|
const name = values.get("name")?.trim() ?? "";
|
|
262
289
|
const about = values.get("about")?.trim() ?? "";
|
|
263
|
-
|
|
264
|
-
throw new UsageError("--name is required, up to 60 characters");
|
|
290
|
+
// What the brain keeps, in UTF-16 units as a JavaScript string counts them: one for nearly all text, two or more for an emoji.
|
|
265
291
|
if (name.length > 60)
|
|
266
|
-
throw new UsageError("--name is at most 60 characters");
|
|
292
|
+
throw new UsageError("--name is at most 60 characters, counted as UTF-16 units: one for nearly all text, two or more for an emoji");
|
|
267
293
|
// A name that hides characters or turns text around can read as another, Jev's included.
|
|
268
294
|
const nameHides = unseen(name);
|
|
269
295
|
if (nameHides)
|
|
270
296
|
throw new UsageError(`--name must be one line with no invisible or direction-changing characters, and it holds ${nameHides}`);
|
|
271
|
-
if (!about) {
|
|
272
|
-
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");
|
|
273
|
-
}
|
|
274
297
|
if (about.length > 500)
|
|
275
|
-
throw new UsageError("--about is at most 500 characters");
|
|
298
|
+
throw new UsageError("--about is at most 500 characters, counted as UTF-16 units: one for nearly all text, two or more for an emoji");
|
|
276
299
|
// A description may run over several lines.
|
|
277
300
|
const aboutHides = unseen(about.replace(/[\t\n]/g, " "));
|
|
278
301
|
if (aboutHides)
|
|
@@ -286,6 +309,21 @@ export async function join(chain, network, values) {
|
|
|
286
309
|
const requested = values.has("price") ? price(values.get("price")) : undefined;
|
|
287
310
|
const standing = await snapshot(chain);
|
|
288
311
|
const { allowed, w, change, pending, open, applying, toStake } = standing;
|
|
312
|
+
// Only an application carries a name and a description, so a worker already allowed or waiting needs neither.
|
|
313
|
+
if (applying) {
|
|
314
|
+
if (!name)
|
|
315
|
+
throw new UsageError("--name is required, up to 60 characters");
|
|
316
|
+
if (!about)
|
|
317
|
+
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");
|
|
318
|
+
if (await chain.applicationVetoed())
|
|
319
|
+
throw new Error("The guardian vetoed setWorkerAllowed on the task board, so the executor does not queue an application until the admin reopens that function. Nothing was sent.");
|
|
320
|
+
const taken = await chain.openApplication();
|
|
321
|
+
if (taken > 0n)
|
|
322
|
+
throw new Error(`Proposal ${taken} already asks to allow this worker and is still open, and the contract takes one at a time. Nothing was sent. Follow it with \`npx brainchain-worker proposal ${taken}\`.`);
|
|
323
|
+
}
|
|
324
|
+
else if (name || about) {
|
|
325
|
+
console.log("This worker is past its application, so --name and --about are not used.");
|
|
326
|
+
}
|
|
289
327
|
// The agent id travels in the application, and Jev keeps the profile it decided on.
|
|
290
328
|
if (agentId !== undefined) {
|
|
291
329
|
if (allowed || (change && pending) || open) {
|
|
@@ -366,7 +404,7 @@ export async function join(chain, network, values) {
|
|
|
366
404
|
"the application with `npx brainchain-worker status`. New workers are offered house tasks first, which BrainChain pays for.");
|
|
367
405
|
}
|
|
368
406
|
async function status(chain, network) {
|
|
369
|
-
const [gas, gasPrice, sent, s, held, record, bountyBalance, stakeBalance, bond, faucet, now] = await Promise.all([
|
|
407
|
+
const [gas, gasPrice, sent, s, held, record, bountyBalance, stakeBalance, bond, faucet, now, oldStakes] = await Promise.all([
|
|
370
408
|
chain.ethBalance(),
|
|
371
409
|
chain.gasPrice(),
|
|
372
410
|
chain.sent(),
|
|
@@ -379,12 +417,13 @@ async function status(chain, network) {
|
|
|
379
417
|
// A stake token without a faucet, as on mainnet, leaves out what the faucet would give.
|
|
380
418
|
network.buyStake ? undefined : chain.faucet().catch(() => undefined),
|
|
381
419
|
chain.now(),
|
|
420
|
+
chain.retiredStakes(),
|
|
382
421
|
]);
|
|
383
422
|
const { w, min } = s;
|
|
384
423
|
const unstaking = w.unstaking > 0n ? `, ${stake(network, w.unstaking)} unstaking until ${when(w.unlocksAt)}` : "";
|
|
385
424
|
console.log(`Worker ${chain.account} on ${network.name}
|
|
386
425
|
Hireable ${hireable(s, network, "status")}
|
|
387
|
-
Stake ${stake(network, w.staked)} (minimum ${stake(network, min)})${unstaking}
|
|
426
|
+
Stake ${stake(network, w.staked)} (minimum ${stake(network, min)})${unstaking}${oldStakes.map((o) => `\n Old stake ${stake(network, o.w.staked + o.w.unstaking)} on set ${o.release}, which does not count here; leave and withdraw collect it`).join("")}
|
|
388
427
|
Wallet ${wallet(network, s, stakeBalance, bond, faucet, now)}
|
|
389
428
|
Price ${w.registered ? `${usdg(network, w.price)} per task` : "not set until you join"}
|
|
390
429
|
Record ${record.paid} paid, ${record.refunded} refunded, ${record.inFlight.length} in flight${held > 0 ? " (stake held until they settle)" : ""}${record.inFlight.map((f) => `\n ${flightLine(f)}`).join("")}
|
|
@@ -584,6 +623,12 @@ export function watching(network, address, veto) {
|
|
|
584
623
|
return `watching ${network.name} (read-only, no key)`;
|
|
585
624
|
return `watching ${network.name} as ${address}${veto ? ", vetoing through the guardian gate" : ""}, reporting rounds to the service`;
|
|
586
625
|
}
|
|
626
|
+
/** `run` starts the worker's command in its own process group, which Windows does not have, and loses its output there. */
|
|
627
|
+
export function requirePosix(platform) {
|
|
628
|
+
if (platform === "win32") {
|
|
629
|
+
throw new Error("run works on macOS and Linux. On Windows, run the worker inside WSL: https://learn.microsoft.com/windows/wsl/install. Every other command works on Windows as it is.");
|
|
630
|
+
}
|
|
631
|
+
}
|
|
587
632
|
export async function main(argv) {
|
|
588
633
|
const [command, ...rest] = argv;
|
|
589
634
|
if (command === undefined || command === "help" || command === "--help" || command === "-h") {
|
|
@@ -611,6 +656,8 @@ export async function main(argv) {
|
|
|
611
656
|
return;
|
|
612
657
|
}
|
|
613
658
|
const { positional, values, switches } = parse(command, rest);
|
|
659
|
+
if (command === "run")
|
|
660
|
+
requirePosix(process.platform);
|
|
614
661
|
const network = resolveNetwork(process.env);
|
|
615
662
|
if (process.env.BRAINCHAIN_RPC_URL?.trim()) {
|
|
616
663
|
await requireChainId(network, () => createPublicClient({ transport: http(network.rpc, { timeout: 10_000, retryCount: 0 }) }).getChainId());
|
|
@@ -657,6 +704,8 @@ export async function main(argv) {
|
|
|
657
704
|
}
|
|
658
705
|
if (!usdc)
|
|
659
706
|
throw new UsageError("--usdc is required: how much USDC on Solana to use, such as --usdc 5");
|
|
707
|
+
if (gasUsdc && !switches.has("stake") && parseUnits(gasUsdc, 6) > parseUnits(usdc, 6))
|
|
708
|
+
throw new UsageError(`--gas-usdc ${gasUsdc} is more than --usdc ${usdc}, the most this command may spend`);
|
|
660
709
|
if (network.id !== "mainnet" || !network.bridge)
|
|
661
710
|
throw new Error("fund --from-solana is mainnet only: Relay connects Solana with Robinhood Chain mainnet, not the testnets");
|
|
662
711
|
const worker = signer();
|
|
@@ -671,17 +720,9 @@ export async function main(argv) {
|
|
|
671
720
|
...(network.bridge.tokens?.stake ? { stakeToken: network.bridge.tokens.stake } : {}),
|
|
672
721
|
dry: switches.has("dry"),
|
|
673
722
|
log: console.log,
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
if (eth <= PARENT_ETH_KEEP)
|
|
678
|
-
return console.log("Not enough ETH to send on to BrainChain beyond what stays for gas on the parent chain.");
|
|
679
|
-
return bridge(network, bridgeChain, { asset: "eth", amount: formatEther(eth - PARENT_ETH_KEEP), dry: false, log: console.log });
|
|
680
|
-
}
|
|
681
|
-
const held = await bridgeChain.tokenBalance("stake");
|
|
682
|
-
const { decimals } = assetOf(network, "stake");
|
|
683
|
-
return bridge(network, bridgeChain, { asset: "stake", amount: formatUnits(held, decimals), dry: false, log: console.log });
|
|
684
|
-
},
|
|
723
|
+
parentEth: () => bridgeChain.parentEth(),
|
|
724
|
+
parentStake: () => bridgeChain.tokenBalance("stake"),
|
|
725
|
+
bridgeOn: (asset, amount) => bridge(network, bridgeChain, { asset, amount: formatUnits(amount, assetOf(network, asset).decimals), dry: false, log: console.log }),
|
|
685
726
|
});
|
|
686
727
|
}
|
|
687
728
|
if (command === "credit") {
|
|
@@ -711,10 +752,7 @@ export async function main(argv) {
|
|
|
711
752
|
set: { key: "active", release: "", fromBlock: Number(network.fromBlock), board: taskBoard, log, executor },
|
|
712
753
|
briefs: new Map(),
|
|
713
754
|
}, id);
|
|
714
|
-
|
|
715
|
-
// A verified decision is reported with the record it was checked against, fetched again as the service serves it.
|
|
716
|
-
const record = findings.length === 0 && keyed && report.payloadURI ? await fetchRecord(report.payloadURI).catch(() => undefined) : undefined;
|
|
717
|
-
return { findings, ...(record ? { record } : {}) };
|
|
755
|
+
return outcome(report, keyed ? { refetch: fetchRecord } : {});
|
|
718
756
|
};
|
|
719
757
|
const chain = createWatchChain(network, account);
|
|
720
758
|
const watch = createWatch({
|
|
@@ -747,9 +785,8 @@ export async function main(argv) {
|
|
|
747
785
|
const id = positional[0];
|
|
748
786
|
if (id === undefined || !/^\d{1,18}$/.test(id) || id === "0")
|
|
749
787
|
throw new UsageError("Usage: brainchain-worker proposal <id>, such as proposal 2");
|
|
750
|
-
// Reading a proposal needs no key, so any address stands in for the signer.
|
|
751
|
-
|
|
752
|
-
return proposal(network, createProposals(network, reader), BigInt(id), console.log);
|
|
788
|
+
// Reading a proposal needs no key, so any address stands in for the signer, and a key that is set is never read.
|
|
789
|
+
return proposal(network, createProposals(network, privateKeyToAccount(generatePrivateKey())), BigInt(id), console.log);
|
|
753
790
|
}
|
|
754
791
|
const worker = signer();
|
|
755
792
|
if (command === "dispute" || command === "appeal") {
|
|
@@ -795,6 +832,12 @@ export async function main(argv) {
|
|
|
795
832
|
case "leave": {
|
|
796
833
|
const w = await chain.stake();
|
|
797
834
|
if (w.staked === 0n) {
|
|
835
|
+
const old = (await chain.retiredStakes()).find((o) => o.w.staked > 0n);
|
|
836
|
+
if (old) {
|
|
837
|
+
await needGas(chain, network);
|
|
838
|
+
await chain.requestUnstake(old.w.staked, old.staking);
|
|
839
|
+
return console.log(`Unstaking ${stake(network, old.w.staked)} from set ${old.release}. Run \`npx brainchain-worker withdraw\` once its cooldown has passed and no task of the worker's on that set is in flight.`);
|
|
840
|
+
}
|
|
798
841
|
throw new Error(w.unstaking > 0n ? `Nothing left to unstake: ${stake(network, w.unstaking)} is already unstaking until ${when(w.unlocksAt)}.` : "Nothing is staked.");
|
|
799
842
|
}
|
|
800
843
|
await needGas(chain, network);
|
|
@@ -805,8 +848,16 @@ export async function main(argv) {
|
|
|
805
848
|
}
|
|
806
849
|
case "withdraw": {
|
|
807
850
|
const [w, held, now] = await Promise.all([chain.stake(), chain.held(), chain.now()]);
|
|
808
|
-
if (w.unstaking === 0n)
|
|
809
|
-
|
|
851
|
+
if (w.unstaking === 0n) {
|
|
852
|
+
const old = (await chain.retiredStakes()).find((o) => o.w.unstaking > 0n);
|
|
853
|
+
if (!old)
|
|
854
|
+
throw new Error("Nothing is unstaking. Run leave first.");
|
|
855
|
+
if (now < old.w.unlocksAt)
|
|
856
|
+
throw new Error(`${stake(network, old.w.unstaking)} on set ${old.release} unlocks at ${when(old.w.unlocksAt)}. Withdraw after that.`);
|
|
857
|
+
await needGas(chain, network);
|
|
858
|
+
await chain.withdraw(old.staking);
|
|
859
|
+
return console.log(`Withdrew ${stake(network, old.w.unstaking)} from set ${old.release}.`);
|
|
860
|
+
}
|
|
810
861
|
if (now < w.unlocksAt)
|
|
811
862
|
throw new Error(`${stake(network, w.unstaking)} unlocks at ${when(w.unlocksAt)}. Withdraw after that.`);
|
|
812
863
|
if (held > 0)
|
package/dist/credit.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createPublicClient, http, parseAbi } from "viem";
|
|
1
|
+
import { BaseError, ContractFunctionRevertedError, createPublicClient, http, parseAbi } from "viem";
|
|
2
2
|
import { reason } from "./chain.js";
|
|
3
3
|
import { parentChainOf } from "./network.js";
|
|
4
4
|
export const CONSENT_TYPES = {
|
|
@@ -47,6 +47,9 @@ export async function credit(network, chain, options) {
|
|
|
47
47
|
owner = await chain.ownerOf(agentId);
|
|
48
48
|
}
|
|
49
49
|
catch (error) {
|
|
50
|
+
// The registry reverts ownerOf for an id it never minted; anything else says nothing about the agent.
|
|
51
|
+
if (error instanceof BaseError && error.walk((e) => e instanceof ContractFunctionRevertedError))
|
|
52
|
+
throw new Error(`There is no ERC-8004 agent ${agentId} on Robinhood Chain.`);
|
|
50
53
|
throw new Error(`ERC-8004 agent ${agentId} could not be read on Robinhood Chain: ${reason(error)}`);
|
|
51
54
|
}
|
|
52
55
|
if (owner.toLowerCase() !== chain.account.toLowerCase()) {
|