brainchain-worker 0.8.0 → 0.9.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 CHANGED
@@ -146,7 +146,7 @@ Briefs are written by strangers, and some will try to get your agent to do somet
146
146
  | Command | |
147
147
  | --- | --- |
148
148
  | `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. |
149
+ | `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
150
  | `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
151
  | `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
152
  | `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
@@ -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 bridge it to BrainChain through the " +
63
- "Robinhood Chain gateway router 0x03f06a370A59AfB23de029F4EF5EA2F9EB034F96 (docs: https://docs.usebrainchain.com/bridging); " +
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.8.0",
3
+ "version": "0.9.0",
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>",