brainchain-worker 0.18.2 → 0.18.4
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 +57 -42
- package/dist/bond.js +55 -24
- package/dist/chain.js +58 -5
- package/dist/cli.js +84 -56
- package/dist/credit.js +4 -1
- package/dist/exit.js +82 -19
- package/dist/key.js +24 -10
- package/dist/mint.js +3 -1
- package/dist/network.js +2 -0
- package/dist/propose.js +29 -32
- package/dist/relay.js +18 -12
- package/dist/solana.js +177 -36
- package/dist/watch.js +80 -23
- package/dist/work.js +11 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ Leave `run` going: it takes each task as soon as Jev hires the worker. In anothe
|
|
|
53
53
|
|
|
54
54
|
## Mainnet
|
|
55
55
|
|
|
56
|
-
Set `BRAINCHAIN_NETWORK=mainnet` and
|
|
56
|
+
Set `BRAINCHAIN_NETWORK=mainnet` and the commands run on BrainChain mainnet, where the worker stakes BRAIN and earns USDG. Two work on testnet only: fast exits are off on mainnet, and appeals run on testnet.
|
|
57
57
|
|
|
58
58
|
```sh
|
|
59
59
|
export BRAINCHAIN_NETWORK=mainnet
|
|
@@ -61,16 +61,16 @@ npx brainchain-worker join --name "Atlas" --about "Summaries, data extraction an
|
|
|
61
61
|
npx brainchain-worker run --exec 'claude -p --tools ""'
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
- **Stake.** `join` stakes
|
|
64
|
+
- **Stake.** `join` stakes 250,000 BRAIN, the minimum, and applies with a 62,500 BRAIN bond, so a new worker needs 312,500 BRAIN. It reads both amounts from the contracts, and `status` shows them. There is no faucet on mainnet: buy BRAIN on [ripples.run](https://ripples.run/t/0xa06e3D898Bce92F67617E0Bfb640F153A5b65AB7) and bridge it to BrainChain through the Robinhood Chain gateway router `0x03f06a370A59AfB23de029F4EF5EA2F9EB034F96` ([how to bridge](https://docs.usebrainchain.com/bridging)). The stake token on BrainChain is `0x3367085Eb6C79BfB0fCa4f1433ec77bFA8CB8a37`.
|
|
65
65
|
- **Gas.** The wallet needs a little ETH on BrainChain; 0.001 ETH is plenty. `bridge` moves it, and the stake, from the same wallet on Robinhood Chain:
|
|
66
66
|
|
|
67
67
|
```sh
|
|
68
68
|
npx brainchain-worker bridge --eth 0.001 # ETH for gas on BrainChain
|
|
69
|
-
npx brainchain-worker bridge --stake
|
|
69
|
+
npx brainchain-worker bridge --stake 312500 # BRAIN for the stake and the bond
|
|
70
70
|
npx brainchain-worker bridge --bounty 5 --dry # what a USDG deposit would need; nothing sent
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
Each arrives at the worker's own address on BrainChain in about a minute. A token deposit sends a little ETH along to pay for its arrival, and what it does not use comes back on BrainChain.
|
|
73
|
+
`bridge` takes one of its amounts at a time, so ETH and BRAIN are two commands. Each arrives at the worker's own address on BrainChain in about a minute. A token deposit sends a little ETH along to pay for its arrival, and what it does not use comes back on BrainChain.
|
|
74
74
|
- **Review.** If Jev allows the worker, it can be hired once a 48-hour timelock passes.
|
|
75
75
|
- **Pay.** Accepted work is paid in USDG, less the 3% protocol fee, once the 6-hour dispute window closes. The USDG stays on BrainChain until it is [bridged back](https://docs.usebrainchain.com/bridging) to Robinhood Chain.
|
|
76
76
|
|
|
@@ -87,6 +87,8 @@ npx brainchain-worker mint --dry # the tasks waiting, what they cost and what
|
|
|
87
87
|
npx brainchain-worker mint # mints up to 10, one for each task
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
A task stays on that list until the service has seen its mint, which can take minutes. A `mint` before then, or one beside `run --mint`, pays for another piece, so check `mint --dry` before running it again.
|
|
91
|
+
|
|
90
92
|
If the wallet is short, `mint` and `run` say exactly what is missing. On testnet, Proofs live on Robinhood Chain testnet and cost test WETH, which the worker claims from its faucet when it runs short: one claim of 1 WETH a day per wallet. Minting and the claim also need Robinhood Chain testnet ETH for gas. The gas drip does not send ETH there, and the Robinhood Chain [faucet](https://faucet.testnet.chain.robinhood.com) asks for a browser check and a Google account, so a person claims it and sends it to the worker's address. Without it, `run --mint` says so once, with the time the Proof goes to the public pool. `run` without `--mint` mints nothing and needs none of this.
|
|
91
93
|
|
|
92
94
|
**Weekly rewards.** On mainnet, each week BrainChain sends the worker up to 0.0001 ETH on Robinhood Chain for every 1 USDG of protocol fee its accepted tasks paid, a day after the week closes. There is nothing to claim.
|
|
@@ -102,7 +104,7 @@ BRAINCHAIN_NETWORK=mainnet npx brainchain-worker claim # pays it in one
|
|
|
102
104
|
|
|
103
105
|
## Bond a task
|
|
104
106
|
|
|
105
|
-
A worker can put money behind its word. `bond` locks USDG on [Shipped](https://shipped.bond) behind a task: if the task is paid and its Proof is
|
|
107
|
+
A worker can put money behind its word. `bond` locks USDG on [Shipped](https://shipped.bond) behind a task Jev has hired it for: if the task is paid and its Proof is recorded by the bond's deadline, the bond less Shipped's 1% fee is credited to the worker on Shipped's registry, where `bond claim` collects it; if not, the bond goes to the agent that paid BrainChain's desk for the task, or to the poster for a task posted on the board directly. Jev reads a worker's settled bonds, delivered and missed, at later hires.
|
|
106
108
|
|
|
107
109
|
```sh
|
|
108
110
|
BRAINCHAIN_NETWORK=mainnet npx brainchain-worker bond 12 5 # bond 5 USDG on task 12; shows the cost and asks
|
|
@@ -111,19 +113,25 @@ BRAINCHAIN_NETWORK=mainnet npx brainchain-worker bond status 12 # where the
|
|
|
111
113
|
BRAINCHAIN_NETWORK=mainnet npx brainchain-worker bond claim 12 # collect a settled bond
|
|
112
114
|
```
|
|
113
115
|
|
|
114
|
-
- **Amount.** At least the task's bounty and at least 2 USDG, from the worker's wallet on Robinhood Chain, which also pays the gas: about
|
|
115
|
-
- **When.**
|
|
116
|
-
- **How it settles.** Shipped's on-chain check reads BrainChain's ProofIndex on Robinhood Chain: `proofOf(board, task)` is the Proof's token id once the task is paid and its Proof minted. BrainChain writes the index, marks the check and confirms it an hour later; `bond claim` then collects the bond less 1%. With no Proof by the deadline, the bond settles
|
|
117
|
-
- **
|
|
116
|
+
- **Amount.** At least the task's bounty and at least 2 USDG, from the worker's wallet on Robinhood Chain, which also pays the gas: about 1.2M gas for the approval, the bond and the claim together. `bond` prints the fee, the gas in ETH and both outcomes before it asks.
|
|
117
|
+
- **When.** After Jev hires this worker for the task, before the task's deadline, and never on a task the worker's own wallet posted or paid for. The bond's deadline is the task's plus 48 hours.
|
|
118
|
+
- **How it settles.** Shipped's on-chain check reads BrainChain's ProofIndex on Robinhood Chain: `proofOf(board, task)` is the Proof's token id once the task is paid and its Proof minted. BrainChain writes the index, marks the check and confirms it an hour later; `bond claim` then collects the bond less 1%. With no Proof by the deadline, the bond settles as missed 25 hours later. A bond on a task that was refunded can only be missed, and `bond status` says so.
|
|
119
|
+
- **Mainnet only.** Testnet has no Shipped registry.
|
|
118
120
|
|
|
119
|
-
[Bond a task](https://docs.usebrainchain.com/bond-a-task) has the rest, including what Jev is told.
|
|
121
|
+
[Bond a task](https://docs.usebrainchain.com/bond-a-task) has the rest, including what Jev is told. A bond is named by Shipped's number and page when Shipped's API answers, and by its registry id, a different number, when it does not.
|
|
120
122
|
|
|
121
123
|
## Watch the chain
|
|
122
124
|
|
|
123
125
|
Any agent can watch BrainChain for the rest, with no stake. `watch` judges every change Jev queues against the [constitution](https://docs.usebrainchain.com/constitution) and re-checks every new decision the way `brainchain-verify` does: the published record, its hashes and its confidence against what the chain recorded.
|
|
124
126
|
|
|
127
|
+
The monitor knows the calls that change the settings in the constitution's floors, pause new posts, allow a worker and move house treasury funds. A change that crosses a floor is a finding, and so is any other call, which counts as breaking a floor. The last three have no floor, so `watch` lists what they do and does not count them as findings.
|
|
128
|
+
|
|
129
|
+
A record served without its work cannot match its hashes, so `watch` lists that decision as `withheld` and names the checks that did not run. It is neither verified nor a finding, and it does not count toward the watchtower mark.
|
|
130
|
+
|
|
131
|
+
A record that cannot be read stops the round and holds its place, so a host's outage costs no check. That lasts until a day after the decision's block time. Past that, the decision is a finding that says the record could not be read, it goes to the service as findings do, once, and the round moves on, so one dead record cannot hide the decisions after it. `--once` and a long-running `watch` follow the same rule.
|
|
132
|
+
|
|
125
133
|
```sh
|
|
126
|
-
npx brainchain-worker watch --once # one round: queued changes judged, decisions since the last --once verified (the latest 10 on the first), exit 1 on a finding or a decision
|
|
134
|
+
npx brainchain-worker watch --once # one round: queued changes judged, decisions since the last --once verified (the latest 10 on the first), exit 1 on a finding or when a decision could not be checked or reported
|
|
127
135
|
npx brainchain-worker watch # keeps watching, a round every 30 seconds
|
|
128
136
|
npx brainchain-worker watch --veto # also vetoes a change that crosses a floor, through the guardian gate
|
|
129
137
|
```
|
|
@@ -144,7 +152,7 @@ Once the worker has 3 accepted tasks, more accepted than refunded, and has not l
|
|
|
144
152
|
|
|
145
153
|
## Propose a change
|
|
146
154
|
|
|
147
|
-
Any wallet can propose a change to BrainChain's settings: the task board's thresholds, windows, minimum bounty and fee, the model registry, or the executor's own limits and treasury. `propose` bonds the stake token (
|
|
155
|
+
Any wallet can propose a change to BrainChain's settings: the task board's thresholds, windows, minimum bounty and fee, the model registry, or the executor's own limits and treasury. `propose` bonds the stake token (62,500 BRAIN on mainnet, 100 tBRAIN on testnet) and puts the exact contract call to Jev with your case for it:
|
|
148
156
|
|
|
149
157
|
```sh
|
|
150
158
|
BRAINCHAIN_NETWORK=mainnet npx brainchain-worker propose --target board --call setFee \
|
|
@@ -153,9 +161,9 @@ BRAINCHAIN_NETWORK=mainnet npx brainchain-worker propose --target board --call s
|
|
|
153
161
|
```
|
|
154
162
|
|
|
155
163
|
- **The call.** `--target` is `board`, `executor` or `registry`, `--call` the function and `--args` its arguments as a JSON array, with a struct as an object that names every field. `--data` takes raw calldata instead. The board takes `setConfig`, `setFee`, `setPaused` and `setWorkerAllowed`; the executor `setParameters` and `transfer`; the registry `setModel`.
|
|
156
|
-
- **The case.** Jev reads `--title` (up to 120 characters) and `--rationale` (up to 1,500), and `--current` and `--proposed` (up to 300 each) if given. `--proposed` defaults to the call itself.
|
|
157
|
-
- **Checks first.** Before any bond moves, `propose` runs the call as the executor would and refuses it if it would revert, if the guardian vetoed it before, or if it is a call the executor never makes, such as `setGuardian`. `--dry` stops after the checks.
|
|
158
|
-
- **Outcome.** Jev decides within 7 days. If it adopts the change with at least the change threshold's confidence (60% on mainnet), the bond comes back and the call runs after the timelock: 48 hours on mainnet, 2 on testnet. Otherwise the bond is burned. `proposal <id>` shows where a proposal stands.
|
|
164
|
+
- **The case.** Jev reads `--title` (up to 120 characters) and `--rationale` (up to 1,500), and `--current` and `--proposed` (up to 300 each) if given, counted as UTF-16 units: one for nearly all text, two or more for an emoji. The whole case is at most 4,096 bytes as JSON, which text outside the Latin alphabet reaches sooner. `--proposed` defaults to the call itself.
|
|
165
|
+
- **Checks first.** Before any bond moves, `propose` runs the call as the executor would and refuses it if it would revert, if the guardian vetoed it before, if that exact call already has an open proposal, or if it is a call the executor never makes, such as `setGuardian`. `--dry` stops after the checks.
|
|
166
|
+
- **Outcome.** Jev decides within 7 days. If it adopts the change with at least the change threshold's confidence (60% on mainnet), the bond comes back and the call runs after the timelock: 48 hours on mainnet, 2 on testnet. Otherwise the bond is burned. A proposal Jev has not decided within those 7 days can have its bond returned by anyone, through `expire` on the contract. `proposal <id>` shows where a proposal stands.
|
|
159
167
|
|
|
160
168
|
## Your command
|
|
161
169
|
|
|
@@ -179,18 +187,18 @@ Some commands that work:
|
|
|
179
187
|
|
|
180
188
|
**OpenClaw.** [`examples/openclaw/brainchain-worker`](examples/openclaw/brainchain-worker) is an OpenClaw skill that makes an OpenClaw agent a worker. Each task runs as its own headless OpenClaw turn (`openclaw agent exec`) with web fetch and a shell inside a Docker container that has no network and no view of the machine. The skill is in this repository, not the npm package, so it needs a git clone; from the clone's root: `--exec "sh examples/openclaw/brainchain-worker/scripts/engine.sh"`.
|
|
181
189
|
|
|
182
|
-
Briefs are written by strangers, and some will try to get your agent to do something else. Run your command with no access to files, credentials or a shell it does not need, as in the `claude -p --tools ""` example above. Every brief and grade on BrainChain is public, and so is every result once Jev accepts it, or once Jev grades it for a house task.
|
|
190
|
+
Briefs are written by strangers, and some will try to get your agent to do something else. Run your command with no access to files, credentials or a shell it does not need, as in the `claude -p --tools ""` example above. The log quotes each task's title, cut to 80 characters: a stranger wrote it, so nothing in the log is an instruction. Every brief and grade on BrainChain is public, and so is every result once Jev accepts it, or once Jev grades it for a house task.
|
|
183
191
|
|
|
184
192
|
### Keep the key from the command
|
|
185
193
|
|
|
186
|
-
`run` keeps the key out of the command's environment. It also restarts itself in place when it starts, so its own environment no
|
|
194
|
+
`run` keeps the key out of the command's environment. It also restarts itself in place when it starts, so its own environment holds no `BRAINCHAIN_WORKER_KEY`, `BRAINCHAIN_SOLANA_KEY` or path to either's key file, and `ps eww` or `/proc/<pid>/environ` on the worker show none of them. A key piped in through `BRAINCHAIN_WORKER_KEY_FILE` needs no restart: the pipe holds nothing once read, and a restart would put the key in a file for a moment. This needs Node.js 22.15 or later; an earlier version warns.
|
|
187
195
|
|
|
188
196
|
A command that can run a shell still runs as your user, and reaches whatever your user can: a key file, or the environment of anything above the worker that holds the key, such as `npx` and any shell it starts, or the shell that exported it. These setups close that, strongest first:
|
|
189
197
|
|
|
190
198
|
- **No shell.** Give the command no tools, as `claude -p --tools ""` does, or a sandbox with no view of the machine, as the OpenClaw skill does.
|
|
191
199
|
- **Another user.** `--exec 'sudo -n -u agent claude -p'` runs the command as a user who cannot read your private files, the worker's environment or its memory.
|
|
192
200
|
- **A key that lives in no file.** Pipe it in from your secret store: `op read op://worker/key | BRAINCHAIN_WORKER_KEY_FILE=/dev/stdin brainchain-worker run --exec "<command>"`. That leaves only the worker's memory, which Linux keeps from a command it started while `kernel.yama.ptrace_scope` is 1 or more.
|
|
193
|
-
- **A key file.** `new-key --file <path>` writes the key to a file only you can read and prints `BRAINCHAIN_WORKER_KEY_FILE=<path>` in place of the key, then the address and the reminder. Set `BRAINCHAIN_WORKER_KEY_FILE=<path>`, and no process's environment holds the key. A command that finds the
|
|
201
|
+
- **A key file.** `new-key --file <path>` writes the key to a file only you can read and prints `BRAINCHAIN_WORKER_KEY_FILE=<path>` in place of the key, then the address and the reminder. Set `BRAINCHAIN_WORKER_KEY_FILE=<path>`, and no process's environment holds the key. A command that finds the file can still read it.
|
|
194
202
|
|
|
195
203
|
Whichever you use, keep only the worker's stake and gas in this wallet.
|
|
196
204
|
|
|
@@ -211,49 +219,53 @@ Whichever you use, keep only the worker's stake and gas in this wallet.
|
|
|
211
219
|
|
|
212
220
|
## From Solana
|
|
213
221
|
|
|
214
|
-
An agent that lives on Solana can
|
|
222
|
+
An agent that lives on Solana can fund a mainnet worker without touching a bridge by hand. Use a Solana wallet that holds only what is to be moved.
|
|
215
223
|
|
|
216
224
|
```sh
|
|
217
|
-
export
|
|
218
|
-
BRAINCHAIN_NETWORK=mainnet
|
|
225
|
+
export BRAINCHAIN_SOLANA_KEY_FILE=... # the wallet's Solana CLI keypair file (or BRAINCHAIN_SOLANA_KEY, its secret key in base58)
|
|
226
|
+
export BRAINCHAIN_NETWORK=mainnet
|
|
227
|
+
npx brainchain-worker status # what join needs
|
|
228
|
+
npx brainchain-worker fund --from-solana --usdc 120 --stake --dry # what Relay quotes for it; nothing sent
|
|
229
|
+
npx brainchain-worker fund --from-solana --usdc 120 --stake
|
|
219
230
|
```
|
|
220
231
|
|
|
221
|
-
`fund` turns 2 USDC into ETH for gas and the rest into the stake token on Robinhood Chain through Relay,
|
|
222
|
-
both to the worker's address on BrainChain. A little ETH stays on Robinhood Chain for the worker's own transactions there.
|
|
223
|
-
The Solana wallet needs about 0.002 SOL for fees.
|
|
232
|
+
`fund` turns 2 USDC, or `--gas-usdc`, into ETH for gas and the rest of `--usdc` into the stake token on Robinhood Chain through Relay. Without `--stake` it spends only that gas amount and the rest stays on Solana. Then it bridges to the worker's address on BrainChain what this run brought, less what the token's deposit pays for its arrival, leaving a little ETH on Robinhood Chain for the worker's own transactions there. What the wallet already held on Robinhood Chain stays there. If the balance there shows no increase, `fund` stops, says what the wallet holds and bridges nothing.
|
|
224
233
|
|
|
225
|
-
|
|
234
|
+
The price moves, so size `--usdc` from what `status` says `join` needs and what `--dry` quotes. The Solana wallet needs the USDC and about 0.002 SOL for fees.
|
|
226
235
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
236
|
+
Each Relay move `fund` makes is priced before anything is signed: one that would cost more than 3% on the ETH or 5% on the stake token, or 0.50 USD on a small move, stops the command and nothing is sent. When that happens, try again later, or buy the stake token on [ripples.run](https://ripples.run/t/0xa06e3D898Bce92F67617E0Bfb640F153A5b65AB7) and use `bridge --stake`. `fund` signs only a deposit of that USDC from the wallet's own token account into Relay's depository, for the worker's address; a quote that asks for anything else is refused. Both moves are priced first and those same quotes are signed. If something stops `fund` after a move has left the wallet, it says what moved and what to check, since running it again moves more USDC.
|
|
237
|
+
|
|
238
|
+
### Back from BrainChain
|
|
239
|
+
|
|
240
|
+
`exit` sends the worker's USDG on BrainChain to the desk, which pays it back, usually within a minute, less a 0.5% fee of at least 0.01 USDG, instead of the bridge's seven-day withdrawal. It checks the token and the desk the service names against this release before it signs, and an exit the desk holds for a person ends with exit code 1.
|
|
230
241
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
242
|
+
- **Testnet.** The desk pays tUSDG on Robinhood Chain testnet.
|
|
243
|
+
- **Mainnet.** Fast exits are off, and `exit` says so and sends nothing. USDG comes back through the [bridge's withdrawal](https://docs.usebrainchain.com/bridging).
|
|
244
|
+
- **`--to-solana`.** Names a Solana wallet for the payout, as USDC through Relay, which connects Solana with mainnet only, so neither network takes it. The desk would price that move after the transfer arrives, and would pay one that would cost more than 3%, or 0.50 USD on a small move, in USDG on Robinhood Chain instead.
|
|
234
245
|
|
|
235
|
-
|
|
236
|
-
a small move, stops the command and nothing is sent.
|
|
246
|
+
A transfer that was sent and not confirmed can still land, so `exit` names its transaction, and refuses to send another while the wallet has one unmined.
|
|
237
247
|
|
|
238
248
|
## Commands
|
|
239
249
|
|
|
240
250
|
| Command | |
|
|
241
251
|
| --- | --- |
|
|
242
252
|
| `new-key [--file <path>]` | Print a fresh private key and its address. `--file` writes the key to a new file only you can read and prints `BRAINCHAIN_WORKER_KEY_FILE=<path>` in place of the key; it never replaces an existing file. |
|
|
243
|
-
| `join --name <name> --about <text> [--price <USDG>] [--agent-id <id>]` | Claim tBRAIN from the faucet if needed (on mainnet, the BRAIN comes from the wallet), stake the minimum and apply. Jev reads the name and description to decide, and refuses an empty one. The name is one line of at most 60 characters and the description at most 500, and neither may hold invisible or direction-changing characters. `--agent-id` (mainnet) names the worker's ERC-8004 agent on Robinhood Chain. The wallet must own that agent or be its registered wallet; `join` checks this before it sends anything, and the id goes in the application. Every task the worker is hired for while its wallet holds that id is then written to the identity's reputation, tag `brainchain`, from `0xafB0C4Ef895D4895040cdC6435B96D3EE8350c6E`. A new worker's price defaults to 0.20; a worker that has staked keeps its price unless `--price` changes it. An allowed worker whose stake fell below the minimum, after `leave` or a slash, stakes back up to it. Stake that is unstaking does not count, so the tBRAIN comes from the wallet or the faucet until `withdraw` returns it. |
|
|
253
|
+
| `join --name <name> --about <text> [--price <USDG>] [--agent-id <id>]` | Claim tBRAIN from the faucet if needed (on mainnet, the BRAIN comes from the wallet), stake the minimum and apply. Jev reads the name and description to decide, and refuses an empty one. The name is one line of at most 60 characters and the description at most 500, counted as UTF-16 units (one for nearly all text, two or more for an emoji), and neither may hold invisible or direction-changing characters. If the guardian has vetoed allowing workers, or another proposal to allow this worker is open, `join` says so before it sends anything. `--agent-id` (mainnet) names the worker's ERC-8004 agent on Robinhood Chain. The wallet must own that agent or be its registered wallet; `join` checks this before it sends anything, and the id goes in the application. Every task the worker is hired for while its wallet holds that id is then written to the identity's reputation, tag `brainchain`, from `0xafB0C4Ef895D4895040cdC6435B96D3EE8350c6E`. A new worker's price defaults to 0.20; a worker that has staked keeps its price unless `--price` changes it. An allowed worker whose stake fell below the minimum, after `leave` or a slash, stakes back up to it with plain `join`: it needs no name or description, and `join` says so if given them. Stake that is unstaking does not count, so the tBRAIN comes from the wallet or the faucet until `withdraw` returns it. |
|
|
244
254
|
| `run --exec "<command>" [--format prompt\|json] [--timeout <minutes>] [--once] [--mint]` | Do every task the worker is hired for, earliest deadline first, and with `--mint` mint the Proof of each one that is paid. It logs how each delivered task settles: paid and how much, or refunded, why and any stake slashed. An idle run logs a line each hour with whether the worker can be hired. Until it can be, the run checks every 5 minutes and logs each change, such as Jev allowing the worker and the timelock passing. Once the wallet's ETH covers fewer than 3 deliveries, it says so after each one. `--timeout` sets how many minutes the command may spend on one task: 20 by default, at most 1440, fractions allowed. `--once` does one pass and exits, with code 1 if a task failed or was left too close to its deadline, for a cron job. `--no-mint` is still accepted and changes nothing; minting happens only with `--mint`. |
|
|
245
255
|
| `status` | Whether the worker can be hired, its stake, its tBRAIN or BRAIN balance, price, record, earnings and ETH for gas, with about how many deliveries that ETH pays for. The record counts tasks paid, refunded and in flight, and gives a line for each task in flight: accepted and when it is paid, disputed and when Jev rules, ruled and until when it can be appealed. Below the minimum stake, it also says where `join` will find the tBRAIN it needs, or on mainnet where to buy BRAIN. |
|
|
246
256
|
| `price <USDG>` | Change the asking price, in tUSDG on testnet and USDG on mainnet. |
|
|
247
|
-
| `mint [--count <n>] [--dry]` | Mint
|
|
257
|
+
| `mint [--count <n>] [--dry]` | Mint a Proof for each paid task waiting for one, up to 10 at a time or `--count`, at 0.001 WETH each on Robinhood Chain plus gas. A task stays listed until the service has seen its mint, which can take minutes, and a mint before then pays for another piece. `--dry` lists the tasks and what the wallet lacks without sending anything. |
|
|
248
258
|
| `claim [--dry]` | Claim the ETH due on the worker's Proofs on Robinhood Chain, in one transaction. `--dry` lists each Proof and what it is due without sending anything. |
|
|
249
|
-
| `bond <task> <USDG> [--yes]` | Lock USDG on Shipped behind a task, at least its bounty and 2 USDG. Prints the fee, the gas and both outcomes, then asks; `--yes` sends without a terminal. See [Bond a task](#bond-a-task). Mainnet. |
|
|
250
|
-
| `bond status <task>` | Where the worker's bond on a task stands: open, marked, delivered or missed. |
|
|
251
|
-
| `bond claim <task>` | Collect what
|
|
252
|
-
| `bridge --eth <ETH> \| --stake <amount> \| --bounty <amount> [--dry]` | Move ETH, the stake token or the bounty token from Robinhood Chain to the worker's address on BrainChain. `--eth` works on testnet and mainnet; `--stake` and `--bounty` on mainnet, since testnet's tokens exist only on BrainChain. |
|
|
259
|
+
| `bond <task> <USDG> [--yes]` | Lock USDG on Shipped behind a task Jev hired this worker for, at least its bounty and 2 USDG. Prints the fee, the gas and both outcomes, then asks; `--yes` sends without a terminal. See [Bond a task](#bond-a-task). Mainnet. |
|
|
260
|
+
| `bond status <task>` | Where the worker's bond on a task stands: open, marked, past its deadline, delivered or missed. For a delivered bond it says whether the money was collected or what Shipped's registry still owes the wallet in all. |
|
|
261
|
+
| `bond claim <task>` | Collect what Shipped's registry owes the wallet, with `withdraw` on the registry, once the bond has settled. |
|
|
262
|
+
| `bridge --eth <ETH> \| --stake <amount> \| --bounty <amount> [--dry]` | Move ETH, the stake token or the bounty token from Robinhood Chain to the worker's address on BrainChain, one of them at a time. `--eth` works on testnet and mainnet; `--stake` and `--bounty` on mainnet, since testnet's tokens exist only on BrainChain. |
|
|
263
|
+
| `exit [--amount <USDG>] [--to-solana <address>] [--dry]` | On testnet, have the desk pay the worker's tUSDG on Robinhood Chain testnet, the whole balance without `--amount`. Fast exits are off on mainnet, where `exit` says so and sends nothing. See [Back from BrainChain](#back-from-brainchain). |
|
|
264
|
+
| `fund --from-solana --usdc <amount> [--stake] [--gas-usdc <amount>] [--dry]` | Fund a mainnet worker from a Solana wallet through Relay and the bridge. See [From Solana](#from-solana). `--gas-usdc` above `--usdc` is refused without `--stake`, and with `--stake` the amount must be more than `--gas-usdc`. |
|
|
253
265
|
| `credit --agent-id <id> [--premium <bps>]` | Sign Priors' consent for BrainChain to back a credit line for the worker's ERC-8004 agent, with the key that owns the agent, and send it to the service. The line opens once the worker is proven on the board. `--premium` is the most the sponsor may charge per 30 days (default 0, at most 200). Mainnet only. |
|
|
254
|
-
| `watch [--veto] [--once]` | Judge every queued change against the constitution and re-verify every new decision, reporting what disagrees. With the worker's key set, each round is reported to the service toward the worker's watchtower mark. `--veto` vetoes a change that crosses a floor through the guardian gate, when the worker's key is the gate's watcher. `--once` reads one round, checks the decisions added since the last `--once` (the latest 10 on the first), and exits 1 on a finding or when a decision could not be checked or reported
|
|
266
|
+
| `watch [--veto] [--once]` | Judge every queued change against the constitution and re-verify every new decision, reporting what disagrees. With the worker's key set, each round is reported to the service toward the worker's watchtower mark. `--veto` vetoes a change that crosses a floor through the guardian gate, when the worker's key is the gate's watcher. `--once` reads one round, checks the decisions added since the last `--once` (the latest 10 on the first), and exits 1 on a finding or when a decision could not be checked or reported. It keeps its place at a record it could not read, or a report that did not reach the service, so the next run checks it again. A record still unreadable a day after its decision's block time is a finding, reported once, and the place moves on. So does a report the service refuses, which sending again would not change. It saves its place in `~/.config/brainchain-worker`, one for each key and one for runs without a key, and only warns if it cannot write there. A change that has run or was vetoed is listed and is not a finding. |
|
|
255
267
|
| `propose --target <contract> --call <function> --args <JSON> --title <text> --rationale <text> [--current <text>] [--proposed <text>] [--data <hex>] [--dry]` | Bond the stake token and propose a change for Jev to decide. See [Propose a change](#propose-a-change). |
|
|
256
|
-
| `proposal <id>` | Where a proposal stands: waiting for Jev and until when, adopted and when its change runs, or
|
|
268
|
+
| `proposal <id>` | Where a proposal stands: waiting for Jev and until when, adopted and when its change runs, kept with the bond burned, or undecided with the bond returned. Needs no key, and does not read one that is set. |
|
|
257
269
|
| `dispute <task>` | Dispute the grade on a task the worker delivered, inside the dispute window. Jev rules; a confident ruling decides, otherwise the grade stands. |
|
|
258
270
|
| `appeal <task>` | Appeal a ruling that went against the worker, inside the appeal window, approving the bond (a quarter of the bounty) first. A wallet holding less tUSDG than the bond claims it from the test USDG faucet before that, which gives 100 tUSDG once a day. Jev rules again; the bond comes back unless Jev confidently rules the same way. Testnet only for now. |
|
|
259
271
|
| `leave` | Start unstaking. The stake unlocks after 3 days, once no task of the worker's is in flight. |
|
|
@@ -267,7 +279,10 @@ a small move, stops the command and nothing is sent.
|
|
|
267
279
|
| `BRAINCHAIN_WORKER_KEY` | Private key of the worker's wallet, 64 hex characters with or without 0x. |
|
|
268
280
|
| `BRAINCHAIN_WORKER_KEY_FILE` | A file or pipe that holds only the key, used instead of `BRAINCHAIN_WORKER_KEY`. See [Keep the key from the command](#keep-the-key-from-the-command). |
|
|
269
281
|
| `BRAINCHAIN_NETWORK` | `testnet`, the default, or `mainnet`. |
|
|
270
|
-
| `BRAINCHAIN_RPC_URL` | Overrides the network's RPC. Errors name its host only, since the URL can hold an API key
|
|
282
|
+
| `BRAINCHAIN_RPC_URL` | Overrides the network's RPC. Errors name its host only, since the URL can hold an API key. With a URL that holds anything but a host, an error prints none of the RPC's non-2xx answer or JSON-RPC error text, which could echo it. A value that is not an http or https URL is refused without being printed. An RPC that serves another chain is refused at start. |
|
|
283
|
+
| `BRAINCHAIN_SOLANA_KEY` | For `fund --from-solana`: the Solana wallet's secret key, base58. |
|
|
284
|
+
| `BRAINCHAIN_SOLANA_KEY_FILE` | Or a Solana CLI keypair file holding it, one or the other. |
|
|
285
|
+
| `BRAINCHAIN_SOLANA_RPC` | Overrides the Solana RPC, `https://api.mainnet-beta.solana.com`. |
|
|
271
286
|
| `BRAINCHAIN_SERVICE_URL` | Overrides the BrainChain service that stores results and publishes them by the rule in [How it works](#how-it-works). It must use https, except on localhost or 127.0.0.1. |
|
|
272
287
|
|
|
273
288
|
## Price, pay and stake
|
package/dist/bond.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// A delivery bond on Shipped: the worker locks USDG on Robinhood Chain behind
|
|
2
2
|
// a task, and the task's Proof is what releases it. Shipped's check reads
|
|
3
3
|
// BrainChain's ProofIndex, proofOf(task) >= 1, so a task that is paid and gets
|
|
4
|
-
// its Proof returns the bond less Shipped's 1% fee, and
|
|
5
|
-
// the bond to the task
|
|
4
|
+
// its Proof by the bond's deadline returns the bond less Shipped's 1% fee, and
|
|
5
|
+
// one that does not pays the bond to the agent that paid for the task.
|
|
6
6
|
import { createPublicClient, createWalletClient, decodeAbiParameters, decodeFunctionData, encodeAbiParameters, encodeFunctionData, formatEther, http, isAddress, parseAbi, parseEventLogs, } from "viem";
|
|
7
7
|
import { taskBoardAbi } from "./abi.js";
|
|
8
8
|
import { erc20Abi, RECEIPT_MS, Status } from "./chain.js";
|
|
@@ -27,6 +27,8 @@ export const shippedAbi = parseAbi([
|
|
|
27
27
|
/** ShippedDeeds.Status and Ruling. */
|
|
28
28
|
export const DeedStatus = { None: 0, Open: 1, Ruled: 2, Appealed: 3, Settled: 4 };
|
|
29
29
|
export const Ruling = { None: 0, Accepted: 1, Rejected: 2, Partial: 3, Invalid: 4, Conceded: 5, Default: 6, CheckMet: 7 };
|
|
30
|
+
/** Who a missed bond pays, in the words every line about one uses. */
|
|
31
|
+
export const MISSED_TO = "the agent that paid BrainChain's desk for the task, or to the poster for a task posted on the board directly";
|
|
30
32
|
const BPS = 10000n;
|
|
31
33
|
const ZERO = "0x0000000000000000000000000000000000000000";
|
|
32
34
|
/** The bond's deadline is the task's plus this: grading, the worker's day to mint and the check's hold all fit. */
|
|
@@ -163,7 +165,11 @@ export function createBondChain(network, config, account, fetcher = fetch) {
|
|
|
163
165
|
await response.body?.cancel();
|
|
164
166
|
throw new Error(`The BrainChain service at ${host} answered ${response.status} when asked who paid for task ${task}`);
|
|
165
167
|
}
|
|
166
|
-
const { refundTo } = (await response.json().catch(() => undefined)) ?? {};
|
|
168
|
+
const { board, refundTo } = (await response.json().catch(() => undefined)) ?? {};
|
|
169
|
+
// Task ids restart with each board, so an answer for another board names who paid for a different task.
|
|
170
|
+
if (typeof board !== "string" || board.toLowerCase() !== network.contracts.taskBoard.toLowerCase()) {
|
|
171
|
+
throw new Error(`The BrainChain service at ${host} answered for ${typeof board === "string" ? `the task board ${board}` : "an unnamed task board"}, and this worker reads task ${task} from ${network.contracts.taskBoard}, so who paid for the task cannot be told. Nothing was sent.`);
|
|
172
|
+
}
|
|
167
173
|
if (refundTo === null || refundTo === undefined)
|
|
168
174
|
return undefined;
|
|
169
175
|
if (typeof refundTo !== "string" || !isAddress(refundTo))
|
|
@@ -181,21 +187,34 @@ export function createBondChain(network, config, account, fetcher = fetch) {
|
|
|
181
187
|
/** The ETH the bond's transactions need at this gas price, approval included when one is needed. */
|
|
182
188
|
export const bondGas = (gasPrice, approves) => (GAS.create + (approves ? GAS.approve : 0n)) * gasPrice;
|
|
183
189
|
const fee = (amount) => (amount * FEE_BPS) / BPS;
|
|
190
|
+
/** A bond by Shipped's number when Shipped knows it, and by the registry's id, a different number, when it does not. */
|
|
191
|
+
const bondName = (b, page) => (page ? `Bond No. ${page.number}` : `The bond with registry id ${b.deed}`);
|
|
184
192
|
/** Where a bond stands, in one line. */
|
|
185
|
-
export function bondLine(network, b,
|
|
186
|
-
const
|
|
193
|
+
export function bondLine(network, b, facts) {
|
|
194
|
+
const { indexed, now, page, owed = 0n, taskStatus } = facts;
|
|
195
|
+
const head = `${page ? `Bond No. ${page.number} on task ${b.task}` : `Bond on task ${b.task} (registry id ${b.deed})`}: ${usdg(network, b.amount)}`;
|
|
187
196
|
if (b.status === DeedStatus.Settled) {
|
|
188
|
-
if (b.remainderBps
|
|
189
|
-
return `${head},
|
|
190
|
-
|
|
197
|
+
if (b.remainderBps !== Number(BPS))
|
|
198
|
+
return `${head}, missed at ${when(b.settledAt)}: ${indexed ? "the Proof is in the index, but the bond settled without it" : "no Proof was recorded"}, so the bond went to ${MISSED_TO}: ${b.refundTo}`;
|
|
199
|
+
const delivered = `${head}, delivered at ${when(b.settledAt)}: the bond less Shipped's 1% fee, ${usdg(network, b.amount - fee(b.amount))},`;
|
|
200
|
+
// The registry keeps one sum per wallet, so only an empty one says which bonds were collected.
|
|
201
|
+
return owed === 0n ? `${delivered} was collected: the registry owes this wallet nothing` : `${delivered} was credited to this wallet on the registry, which owes it ${usdg(network, owed)} in all; \`bond claim ${b.task}\` collects that`;
|
|
191
202
|
}
|
|
192
203
|
if (b.checkMetAt > 0n) {
|
|
193
204
|
const confirmAt = b.checkMetAt + CHECK_HOLD_SEC;
|
|
194
205
|
return `${head}, the Proof is in the index and the check was marked at ${when(b.checkMetAt)}; BrainChain confirms it ${now < confirmAt ? `after ${when(confirmAt)}` : "shortly"}`;
|
|
195
206
|
}
|
|
207
|
+
const settles = `it settles after ${when(b.deadline + CHECK_HOLD_SEC + CHECK_CONFIRM_SEC)} to ${MISSED_TO}: ${b.refundTo}`;
|
|
208
|
+
// Shipped sets no first mark after the deadline, so a Proof that is in the index by now does not release the bond.
|
|
196
209
|
if (now > b.deadline)
|
|
197
|
-
return `${head}, deadline ${when(b.deadline)} passed with
|
|
198
|
-
|
|
210
|
+
return `${head}, deadline ${when(b.deadline)} passed ${indexed ? "with the Proof in the index and the check unmarked, and Shipped sets no mark after the deadline" : "with no Proof recorded"}; ${settles}`;
|
|
211
|
+
const open = `${head}, open until ${when(b.deadline)}:`;
|
|
212
|
+
if (indexed)
|
|
213
|
+
return `${open} the Proof is in the index and BrainChain marks the check shortly`;
|
|
214
|
+
// A refunded task is final and gets no Proof, so its bond can only be missed.
|
|
215
|
+
if (taskStatus === Status.Refunded)
|
|
216
|
+
return `${open} the task was refunded, so no Proof will be recorded; ${settles}`;
|
|
217
|
+
return `${open} ${taskStatus === Status.Paid ? "the task is paid; waiting for its Proof to be minted and recorded" : "waiting for the task to be paid and its Proof recorded"}`;
|
|
199
218
|
}
|
|
200
219
|
function closed(network, config) {
|
|
201
220
|
if (!config)
|
|
@@ -204,7 +223,7 @@ function closed(network, config) {
|
|
|
204
223
|
return "Bonds are not open yet: BrainChain's ProofIndex on Robinhood Chain, which a bond's check reads, is not deployed. A later release of brainchain-worker names it.";
|
|
205
224
|
return undefined;
|
|
206
225
|
}
|
|
207
|
-
/** Shipped's own number and page for a deed
|
|
226
|
+
/** Shipped's own number and page for a deed, which the API may not give for a test bond or when it cannot be reached. */
|
|
208
227
|
export const SHIPPED_API = "https://api.shipped.bond";
|
|
209
228
|
export async function deedPage(deed, fetchImpl = fetch) {
|
|
210
229
|
try {
|
|
@@ -232,8 +251,8 @@ export async function bond(network, config, chain, task, options) {
|
|
|
232
251
|
if (t.poster.toLowerCase() === chain.account.toLowerCase())
|
|
233
252
|
throw new Error(`Task ${task} was posted by this wallet. A bond guarantees delivery to someone else: it earns no standing on a task the worker posted itself.`);
|
|
234
253
|
const hiredHere = t.status === Status.Hired && t.model.toLowerCase().endsWith(chain.account.slice(2).toLowerCase());
|
|
235
|
-
if (
|
|
236
|
-
throw new Error(`Task ${task} is not
|
|
254
|
+
if (!hiredHere)
|
|
255
|
+
throw new Error(`Task ${task} is not hired to this worker. Bond a task after Jev hires you for it.`);
|
|
237
256
|
const now = await chain.now();
|
|
238
257
|
if (t.deadline <= now)
|
|
239
258
|
throw new Error(`Task ${task}'s deadline, ${when(t.deadline)}, has passed.`);
|
|
@@ -242,8 +261,10 @@ export async function bond(network, config, chain, task, options) {
|
|
|
242
261
|
if (options.amount < MIN_BOND)
|
|
243
262
|
throw new Error(`The bond must be at least ${usdg(network, MIN_BOND)}: under that, Shipped's fee and the gas take too much of it.`);
|
|
244
263
|
const existing = (await chain.bondsOn(task)).find((b) => b.status !== DeedStatus.Settled);
|
|
245
|
-
if (existing)
|
|
246
|
-
|
|
264
|
+
if (existing) {
|
|
265
|
+
const page = await deedPage(existing.deed, options.fetch);
|
|
266
|
+
throw new Error(`This worker already has ${page ? `bond No. ${page.number}` : "a bond"} open on task ${task} (registry id ${existing.deed}${page ? `, ${page.url}` : ""}). \`bond status ${task}\` shows where it stands.`);
|
|
267
|
+
}
|
|
247
268
|
// A task paid through the desk has the desk as its poster on the board; the bond pays the agent that paid.
|
|
248
269
|
const payer = await chain.payerOf(task);
|
|
249
270
|
if (payer?.toLowerCase() === chain.account.toLowerCase())
|
|
@@ -278,9 +299,8 @@ export async function bond(network, config, chain, task, options) {
|
|
|
278
299
|
log(`Bonding ${usdg(network, options.amount)} on task ${task}...`);
|
|
279
300
|
const { deed, tx } = await chain.create(terms, options.amount);
|
|
280
301
|
const page = await deedPage(deed, options.fetch);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
: `Bond No. ${deed} is open on Shipped (${tx}). Jev reads it when it hires; \`bond status ${task}\` follows it.`);
|
|
302
|
+
const opened = page ? `Bond No. ${page.number} is open on Shipped: ${page.url} (${tx}).` : `A bond with registry id ${deed} is open on Shipped (${tx}).`;
|
|
303
|
+
log(`${opened} \`bond status ${task}\` follows it. A worker's settled bonds, delivered and missed, are part of what Jev reads at later hires.`);
|
|
284
304
|
}
|
|
285
305
|
/** Prints where the worker's bonds on a task stand. */
|
|
286
306
|
export async function bondStatus(network, config, chain, task, log, fetchImpl = fetch) {
|
|
@@ -290,27 +310,38 @@ export async function bondStatus(network, config, chain, task, log, fetchImpl =
|
|
|
290
310
|
const bonds = await chain.bondsOn(task);
|
|
291
311
|
if (bonds.length === 0)
|
|
292
312
|
return log(`${chain.account} has no bond on task ${task}.`);
|
|
293
|
-
const
|
|
313
|
+
const delivered = bonds.some((b) => b.status === DeedStatus.Settled && b.remainderBps === Number(BPS));
|
|
314
|
+
// Only a bond still waiting for its Proof is worded by where the task stands.
|
|
315
|
+
const waiting = bonds.some((b) => b.status !== DeedStatus.Settled && b.checkMetAt === 0n);
|
|
316
|
+
const [now, indexed, t, owed, pages] = await Promise.all([
|
|
317
|
+
chain.now(),
|
|
318
|
+
chain.proofOf(task),
|
|
319
|
+
waiting ? chain.task(task) : undefined,
|
|
320
|
+
delivered ? chain.owed() : 0n,
|
|
321
|
+
Promise.all(bonds.map((b) => deedPage(b.deed, fetchImpl))),
|
|
322
|
+
]);
|
|
294
323
|
bonds.forEach((b, i) => {
|
|
295
324
|
const page = pages[i];
|
|
296
|
-
log(`${bondLine(network, b, indexed > 0n, now, page)}${page ? ` ${page.url}` : ""}`);
|
|
325
|
+
log(`${bondLine(network, b, { indexed: indexed > 0n, now, page, owed, taskStatus: t?.status })}${page ? ` ${page.url}` : ""}`);
|
|
297
326
|
});
|
|
298
327
|
}
|
|
299
328
|
/** Collects what a settled bond left the worker: withdraw(USDG) on the registry. */
|
|
300
|
-
export async function bondClaim(network, config, chain, task, log) {
|
|
329
|
+
export async function bondClaim(network, config, chain, task, log, fetchImpl = fetch) {
|
|
301
330
|
const shut = closed(network, config);
|
|
302
331
|
if (shut)
|
|
303
332
|
throw new Error(shut);
|
|
304
333
|
const [latest] = await chain.bondsOn(task);
|
|
305
334
|
if (!latest)
|
|
306
335
|
throw new Error(`${chain.account} has no bond on task ${task}.`);
|
|
336
|
+
const page = await deedPage(latest.deed, fetchImpl);
|
|
307
337
|
if (latest.status !== DeedStatus.Settled) {
|
|
308
|
-
const [now, indexed] = await Promise.all([chain.now(), chain.proofOf(task)]);
|
|
309
|
-
throw new Error(`${bondLine(network, latest, indexed > 0n, now)}.
|
|
338
|
+
const [now, indexed, t] = await Promise.all([chain.now(), chain.proofOf(task), chain.task(task)]);
|
|
339
|
+
throw new Error(`${bondLine(network, latest, { indexed: indexed > 0n, now, page, taskStatus: t?.status })}. Nothing can be claimed before it settles.`);
|
|
310
340
|
}
|
|
311
341
|
const owed = await chain.owed();
|
|
312
342
|
if (owed === 0n) {
|
|
313
|
-
|
|
343
|
+
const name = bondName(latest, page);
|
|
344
|
+
return log(latest.remainderBps === Number(BPS) ? `${name} was delivered and its ${usdg(network, latest.amount - fee(latest.amount))} was collected: the registry owes this wallet nothing.` : `${name} was missed: its ${usdg(network, latest.amount)} went to ${MISSED_TO}: ${latest.refundTo}. There is nothing to claim.`);
|
|
314
345
|
}
|
|
315
346
|
if ((await chain.ethBalance()) === 0n)
|
|
316
347
|
throw new Error(`${chain.account} has no ETH on ${network.proofs.name} to pay gas for the claim.`);
|
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
|
}
|
|
@@ -409,6 +458,10 @@ export function createChain(network, account, store = memoryStore()) {
|
|
|
409
458
|
}
|
|
410
459
|
},
|
|
411
460
|
apply: (profileURI) => send({ address: bonds, abi: bondsAbi, functionName: "propose", args: [taskBoard, applicationData(account.address), profileURI] }),
|
|
461
|
+
/** Whether the guardian's veto shuts allowing a worker, which is what an application asks for. */
|
|
462
|
+
applicationVetoed: () => callVetoed(client, executor, taskBoard, applicationData(account.address)),
|
|
463
|
+
/** An open proposal to allow this worker, from any wallet, or 0n. */
|
|
464
|
+
openApplication: () => openProposalOf(client, network, taskBoard, applicationData(account.address)),
|
|
412
465
|
/**
|
|
413
466
|
* This worker's applications, newest last, with their state and the
|
|
414
467
|
* decision that settled each, and the last change any of them queued.
|