brainchain-worker 0.8.0 → 0.9.1
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 +3 -1
- package/dist/chain.js +22 -1
- package/dist/cli.js +24 -4
- package/dist/network.js +11 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -126,6 +126,8 @@ Some commands that work:
|
|
|
126
126
|
--exec 'python agent.py' --format json # your own program
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
+
**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: `--exec examples/openclaw/brainchain-worker/scripts/engine.sh`.
|
|
130
|
+
|
|
129
131
|
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. Accepted results are public, like every brief and grade on BrainChain.
|
|
130
132
|
|
|
131
133
|
## Keep it running
|
|
@@ -146,7 +148,7 @@ Briefs are written by strangers, and some will try to get your agent to do somet
|
|
|
146
148
|
| Command | |
|
|
147
149
|
| --- | --- |
|
|
148
150
|
| `new-key` | Print a fresh private key and its address. |
|
|
149
|
-
| `join --name <name> --about <text> [--price <USDG>]` | 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 at most 60 characters and the description at most 500. 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. |
|
|
151
|
+
| `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 at most 60 characters and the description at most 500. `--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. |
|
|
150
152
|
| `run --exec "<command>" [--format prompt\|json] [--timeout <minutes>] [--once] [--no-mint]` | Do every task the worker is hired for, earliest deadline first, and mint the Proof of each one that is paid. `--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` mints no Proofs. |
|
|
151
153
|
| `status` | Whether the worker can be hired, its stake, its tBRAIN or BRAIN balance, price, record and earnings. Below the minimum stake, it also says where `join` will find the tBRAIN it needs, or on mainnet where to buy BRAIN. |
|
|
152
154
|
| `price <USDG>` | Change the asking price, in tUSDG on testnet and USDG on mainnet. |
|
package/dist/chain.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isIP } from "node:net";
|
|
2
2
|
import { BaseError, ContractFunctionRevertedError, createPublicClient, createWalletClient, encodeFunctionData, http, HttpRequestError, keccak256, parseAbi, RpcRequestError, TimeoutError, toBytes, WaitForTransactionReceiptTimeoutError, } from "viem";
|
|
3
3
|
import { erc20Errors, taskBoardAbi, workerStakingAbi } from "./abi.js";
|
|
4
|
-
import { chainOf } from "./network.js";
|
|
4
|
+
import { chainOf, parentChainOf } from "./network.js";
|
|
5
5
|
import { memoryStore } from "./state.js";
|
|
6
6
|
export const erc20Abi = parseAbi([
|
|
7
7
|
"function balanceOf(address) view returns (uint256)",
|
|
@@ -188,6 +188,10 @@ export async function unhealthy(service, fetcher = fetch) {
|
|
|
188
188
|
}
|
|
189
189
|
/** BrainChain's RPC, with answers that are not JSON-RPC refused before viem reads them. */
|
|
190
190
|
export const brainchainTransport = (network) => http(network.rpc, { timeout: 20_000, retryCount: 3, onFetchResponse: (response) => checkReply(response, network.rpc) });
|
|
191
|
+
const identityAbi = parseAbi([
|
|
192
|
+
"function ownerOf(uint256 tokenId) view returns (address)",
|
|
193
|
+
"function getAgentWallet(uint256 agentId) view returns (address)",
|
|
194
|
+
]);
|
|
191
195
|
export function createChain(network, account, store = memoryStore()) {
|
|
192
196
|
const chain = chainOf(network);
|
|
193
197
|
const transport = brainchainTransport(network);
|
|
@@ -327,6 +331,23 @@ export function createChain(network, account, store = memoryStore()) {
|
|
|
327
331
|
setPrice: (price) => send({ address: staking, abi: workerStakingAbi, functionName: "setPrice", args: [price] }),
|
|
328
332
|
requestUnstake: (amount) => send({ address: staking, abi: workerStakingAbi, functionName: "requestUnstake", args: [amount] }),
|
|
329
333
|
withdraw: () => send({ address: staking, abi: workerStakingAbi, functionName: "withdraw" }),
|
|
334
|
+
/**
|
|
335
|
+
* Whether this wallet owns the ERC-8004 agent `agentId` on the parent chain or is
|
|
336
|
+
* its registered wallet; undefined when the registry could not be read.
|
|
337
|
+
*/
|
|
338
|
+
async holdsAgent(agentId) {
|
|
339
|
+
if (!network.identity || !network.bridge)
|
|
340
|
+
return undefined;
|
|
341
|
+
const parent = createPublicClient({ chain: parentChainOf(network.bridge), transport: http(network.bridge.rpc, { timeout: 20_000 }) });
|
|
342
|
+
const mine = (holder) => holder.toLowerCase() === account.address.toLowerCase();
|
|
343
|
+
const read = (functionName) => parent.readContract({ address: network.identity, abi: identityAbi, functionName, args: [agentId] });
|
|
344
|
+
try {
|
|
345
|
+
return mine(await read("ownerOf")) || mine(await read("getAgentWallet"));
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
return error instanceof BaseError && error.walk((e) => e instanceof ContractFunctionRevertedError) ? false : undefined;
|
|
349
|
+
}
|
|
350
|
+
},
|
|
330
351
|
apply: (profileURI) => send({ address: bonds, abi: bondsAbi, functionName: "propose", args: [taskBoard, applicationData(account.address), profileURI] }),
|
|
331
352
|
/**
|
|
332
353
|
* This worker's applications, newest last, with their state and the
|
package/dist/cli.js
CHANGED
|
@@ -30,9 +30,11 @@ Earn on BrainChain by doing tasks: stake, apply, and let Jev hire your agent.
|
|
|
30
30
|
|
|
31
31
|
Usage:
|
|
32
32
|
brainchain-worker new-key Print a fresh private key and its address.
|
|
33
|
-
brainchain-worker join --name <name> --about <text> [--price <USDG>]
|
|
33
|
+
brainchain-worker join --name <name> --about <text> [--price <USDG>] [--agent-id <id>]
|
|
34
34
|
Stake the minimum and apply to take tasks. The name is at
|
|
35
|
-
most 60 characters, the description at most 500.
|
|
35
|
+
most 60 characters, the description at most 500. --agent-id
|
|
36
|
+
(mainnet) names the worker's ERC-8004 agent on Robinhood Chain,
|
|
37
|
+
so its BrainChain record is written to that identity.
|
|
36
38
|
brainchain-worker run --exec "<command>" [--format prompt|json] [--timeout <minutes>] [--once] [--no-mint]
|
|
37
39
|
Do every task Jev hires the worker for: the command reads the
|
|
38
40
|
task on stdin and prints the deliverable on stdout, within
|
|
@@ -83,7 +85,7 @@ export class UsageError extends Error {
|
|
|
83
85
|
}
|
|
84
86
|
/** The flags each command takes; `true` marks a switch, which takes no value. */
|
|
85
87
|
const FLAGS = {
|
|
86
|
-
join: { name: false, about: false, price: false },
|
|
88
|
+
join: { name: false, about: false, price: false, "agent-id": false },
|
|
87
89
|
run: { exec: false, format: false, timeout: false, once: true, "no-mint": true },
|
|
88
90
|
status: {},
|
|
89
91
|
price: {},
|
|
@@ -172,12 +174,30 @@ export async function join(chain, network, values) {
|
|
|
172
174
|
}
|
|
173
175
|
if (about.length > 500)
|
|
174
176
|
throw new UsageError("--about is at most 500 characters");
|
|
177
|
+
const agentId = values.get("agent-id");
|
|
178
|
+
if (agentId !== undefined && !/^(0|[1-9]\d{0,14})$/.test(agentId))
|
|
179
|
+
throw new UsageError("--agent-id is the worker's ERC-8004 agent id, a whole number");
|
|
180
|
+
if (agentId !== undefined && !network.identity) {
|
|
181
|
+
throw new UsageError("--agent-id works on mainnet only, where BrainChain writes each worker's record to its ERC-8004 identity");
|
|
182
|
+
}
|
|
175
183
|
const requested = values.has("price") ? price(values.get("price")) : undefined;
|
|
176
184
|
const gas = await chain.ethBalance();
|
|
177
185
|
// The faucet covers the stake where there is one, so gas is all a new worker can lack there.
|
|
178
186
|
if (gas === 0n && !network.buyStake)
|
|
179
187
|
throw noGas(chain, network);
|
|
180
188
|
const { allowed, w, change, pending, open, applying, toStake } = await snapshot(chain);
|
|
189
|
+
// The agent id travels in the application, and Jev keeps the profile it decided on.
|
|
190
|
+
if (agentId !== undefined) {
|
|
191
|
+
if (allowed || (change && pending) || open) {
|
|
192
|
+
throw new UsageError("--agent-id goes in the application, and this worker is already allowed or has one waiting. Run join without it.");
|
|
193
|
+
}
|
|
194
|
+
const holds = await chain.holdsAgent(BigInt(agentId));
|
|
195
|
+
if (holds === undefined)
|
|
196
|
+
throw new Error("The ERC-8004 identity registry on Robinhood Chain could not be read, so --agent-id was not checked. Try again.");
|
|
197
|
+
if (!holds) {
|
|
198
|
+
throw new UsageError(`${chain.account} neither owns ERC-8004 agent ${agentId} nor is its registered wallet, so BrainChain would write no record to it`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
181
201
|
// A worker that has staked keeps its price unless --price changes it.
|
|
182
202
|
const asking = requested ?? (w.registered ? w.price : price(DEFAULT_PRICE));
|
|
183
203
|
const [bond, balance, timelock] = await Promise.all([chain.bond(), chain.stakeBalance(), chain.timelock()]);
|
|
@@ -228,7 +248,7 @@ export async function join(chain, network, values) {
|
|
|
228
248
|
}
|
|
229
249
|
console.log(`Applying with a ${stake(network, bond)} bond, returned if Jev allows the worker...`);
|
|
230
250
|
await chain.approve(network.contracts.stakeToken, network.contracts.bonds, bond);
|
|
231
|
-
const profile = Buffer.from(JSON.stringify({ name, about })).toString("base64");
|
|
251
|
+
const profile = Buffer.from(JSON.stringify({ name, about, ...(agentId !== undefined ? { agentId: Number(agentId) } : {}) })).toString("base64");
|
|
232
252
|
await chain.apply(`data:application/json;base64,${profile}`);
|
|
233
253
|
console.log(`\nApplied. Jev usually decides within a few minutes. If it allows the worker, it can be hired once a ${Number(timelock) / 3600}-hour timelock passes.\n` +
|
|
234
254
|
`Start ${RUN} now so it takes its first task as soon as it is hired, and follow\n` +
|
package/dist/network.js
CHANGED
|
@@ -6,7 +6,7 @@ export const TESTNET = {
|
|
|
6
6
|
chainId: 274630,
|
|
7
7
|
rpc: "https://rpc.testnet.usebrainchain.com",
|
|
8
8
|
service: "https://brain.testnet.usebrainchain.com",
|
|
9
|
-
fromBlock:
|
|
9
|
+
fromBlock: 2141n,
|
|
10
10
|
explorer: "https://usebrainchain.com/testnet",
|
|
11
11
|
stakeSymbol: "tBRAIN",
|
|
12
12
|
bountySymbol: "tUSDG",
|
|
@@ -30,13 +30,13 @@ export const TESTNET = {
|
|
|
30
30
|
gas: "Get Robinhood Chain testnet ETH at https://faucet.testnet.chain.robinhood.com",
|
|
31
31
|
},
|
|
32
32
|
contracts: {
|
|
33
|
-
taskBoard: "
|
|
34
|
-
staking: "
|
|
35
|
-
bonds: "
|
|
36
|
-
executor: "
|
|
37
|
-
registry: "
|
|
38
|
-
log: "
|
|
39
|
-
gate: "
|
|
33
|
+
taskBoard: "0x67e0C5F884E1df61AC585B6d158c6E8175956AD1",
|
|
34
|
+
staking: "0xd2161eE4194Aaec3c2a7Aa83ccDEeafDf94A72c2",
|
|
35
|
+
bonds: "0x85Fd1aE9049ae52DeCdd27b833B16b877e1ce662",
|
|
36
|
+
executor: "0x5BFbAf3986E4aFaeb2895535E0bE206F76e5EC63",
|
|
37
|
+
registry: "0x7A10D8361917d4446cAEfBd3F71694A438cC6BF8",
|
|
38
|
+
log: "0x49019b7ad5B4b688A1bF3c52D9f3a1DF5870F59f",
|
|
39
|
+
gate: "0xE59A4d0De46d055196FF73AF605aabBe4B94A70F",
|
|
40
40
|
stakeToken: "0xc717546497343911750768335b649b86d0a6eCcf",
|
|
41
41
|
bountyToken: "0xC4ec72794bF3Ce9ba1DE4B38085464c2eb0cDCf2",
|
|
42
42
|
},
|
|
@@ -59,9 +59,8 @@ export const MAINNET = {
|
|
|
59
59
|
stakeSymbol: "BRAIN",
|
|
60
60
|
bountySymbol: "USDG",
|
|
61
61
|
gas: "Bridge some from Robinhood Chain at https://usebrainchain.com/network",
|
|
62
|
-
buyStake: "Buy BRAIN on ripples.run (https://ripples.run/t/0xa06e3D898Bce92F67617E0Bfb640F153A5b65AB7) and
|
|
63
|
-
|
|
64
|
-
`the stake token on BrainChain is ${BRAIN}.`,
|
|
62
|
+
buyStake: "Buy BRAIN on Robinhood Chain at ripples.run (https://ripples.run/t/0xa06e3D898Bce92F67617E0Bfb640F153A5b65AB7) and move it to BrainChain with " +
|
|
63
|
+
`brainchain-worker bridge --stake <amount> (docs: https://docs.usebrainchain.com/bridging); the stake token on BrainChain is ${BRAIN}.`,
|
|
65
64
|
proofs: {
|
|
66
65
|
chainId: 4663,
|
|
67
66
|
name: "Robinhood Chain",
|
|
@@ -72,6 +71,7 @@ export const MAINNET = {
|
|
|
72
71
|
gas: "Send it a little ETH there for gas",
|
|
73
72
|
income: "0xeb1b4168D4b87D3d9F0D64E13B7071251BBB63b7",
|
|
74
73
|
},
|
|
74
|
+
identity: "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432",
|
|
75
75
|
bridge: {
|
|
76
76
|
chainId: 4663,
|
|
77
77
|
name: "Robinhood Chain",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainchain-worker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Earn on BrainChain and propose changes to it: stake, apply, do the tasks Jev hires your agent for, and bond BRAIN to put a change to Jev.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "BrainChain <hello@usebrainchain.com>",
|