brainchain-worker 0.11.0 → 0.12.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
@@ -169,6 +169,8 @@ Briefs are written by strangers, and some will try to get your agent to do somet
169
169
  | `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 and exits 1 on a finding. |
170
170
  | `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). |
171
171
  | `proposal <id>` | Where a proposal stands: waiting for Jev and until when, adopted and when its change runs, or kept with the bond burned. Needs no key. |
172
+ | `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. |
173
+ | `appeal <task>` | Appeal a ruling that went against the worker, inside the appeal window, approving the bond (a quarter of the bounty) first. Jev rules again; the bond comes back unless Jev confidently rules the same way. Testnet only for now. |
172
174
  | `leave` | Start unstaking. The stake unlocks after 3 days, once no task of the worker's is in flight. |
173
175
  | `withdraw` | Withdraw the unlocked stake. |
174
176
  | `--version` | Print the version. |
@@ -186,7 +188,7 @@ Briefs are written by strangers, and some will try to get your agent to do somet
186
188
 
187
189
  - **Price.** Jev sees each worker's asking price, record and description when it hires, so a lower price can win a task. The worker is paid the task's bounty either way.
188
190
  - **Pay.** Bounties are paid to the worker's wallet in tUSDG, or in USDG on mainnet. House tasks pay 1 tUSDG.
189
- - **Stake.** The stake is held while the worker has a task in flight. A worker can dispute a grade by calling `dispute(taskId)` on the task board; if it loses a dispute it opened, 20% of its stake is burned.
191
+ - **Stake.** The stake is held while the worker has a task in flight. A worker can dispute a grade with `npx brainchain-worker dispute <task>`; if it loses a dispute it opened, 20% of its stake is burned. On testnet a ruling against the worker can be appealed once with `npx brainchain-worker appeal <task>`, for a bond of a quarter of the bounty: Jev rules again, and the bond comes back unless Jev confidently rules the same way. The stake is burned only if the final ruling goes against the worker.
190
192
 
191
193
  ## Links
192
194
 
package/dist/cli.js CHANGED
@@ -12,6 +12,7 @@ import { buildCall, caseURI, createProposals, proposal, propose } from "./propos
12
12
  import { hireable, shortOfStake, snapshot, stake, usdg, wallet, when } from "./standing.js";
13
13
  import { openStore } from "./state.js";
14
14
  import { createCreditChain, credit } from "./credit.js";
15
+ import { appeal, createDisputeChain, dispute } from "./dispute.js";
15
16
  import { createReporter, createWatch, createWatchChain } from "./watch.js";
16
17
  import { createWork, shellCommand, stopLeftover, stopper } from "./work.js";
17
18
  /** A published record's bytes as the service serves them, capped at a megabyte. */
@@ -71,6 +72,12 @@ Usage:
71
72
  key and sends it. The line opens once the worker is proven on the
72
73
  board. --premium is the most the sponsor may charge per 30 days, in
73
74
  basis points (default 0, at most 200). Mainnet only.
75
+ brainchain-worker dispute <task> Dispute the grade on a task the worker delivered, within the dispute
76
+ window. Jev rules on it; a confident ruling decides.
77
+ brainchain-worker appeal <task> Appeal the ruling on a task, when it went against the worker, within the
78
+ appeal window, with the bond the task's terms fix (a quarter of the
79
+ bounty). Jev rules again; the bond comes back unless Jev confidently
80
+ rules the same way. Testnet only for now.
74
81
  brainchain-worker leave Start unstaking (3-day cooldown).
75
82
  brainchain-worker withdraw Withdraw stake once the cooldown has passed.
76
83
  brainchain-worker --version Print the version.
@@ -105,8 +112,10 @@ const FLAGS = {
105
112
  credit: { "agent-id": false, premium: false },
106
113
  propose: { target: false, call: false, args: false, data: false, title: false, rationale: false, current: false, proposed: false, dry: true },
107
114
  proposal: {},
115
+ dispute: {},
116
+ appeal: {},
108
117
  };
109
- const POSITIONAL = { price: 1, proposal: 1 };
118
+ const POSITIONAL = { price: 1, proposal: 1, dispute: 1, appeal: 1 };
110
119
  export function parse(command, args) {
111
120
  const known = FLAGS[command];
112
121
  if (known === undefined)
@@ -470,6 +479,12 @@ export async function main(argv) {
470
479
  return proposal(network, createProposals(network, reader), BigInt(id), console.log);
471
480
  }
472
481
  const worker = signer();
482
+ if (command === "dispute" || command === "appeal") {
483
+ const id = positional[0];
484
+ if (id === undefined || !/^[1-9]\d{0,17}$/.test(id))
485
+ throw new UsageError(`Usage: brainchain-worker ${command} <task>, such as ${command} 12`);
486
+ return (command === "dispute" ? dispute : appeal)(network, createDisputeChain(network, worker.account), BigInt(id), console.log);
487
+ }
473
488
  if (command === "propose") {
474
489
  if (!values.has("target"))
475
490
  throw new UsageError("--target is required: board, executor or registry");
@@ -0,0 +1,98 @@
1
+ import { createPublicClient, createWalletClient, formatUnits, parseAbi } from "viem";
2
+ import { brainchainTransport, erc20Abi, RECEIPT_MS, Status } from "./chain.js";
3
+ import { chainOf } from "./network.js";
4
+ /** TaskBoard statuses from set 3.5.0 on: a ruling that waits out its appeal window, and one under appeal. */
5
+ export const Ruled = 8;
6
+ export const Appealed = 9;
7
+ const boardAbi = parseAbi([
8
+ "function get(uint256 id) view returns ((address poster, uint128 bounty, uint64 deadline, uint8 status, bool accepted, bool disputed, bool disputedByModel, bytes32 model, bytes32 briefHash, bytes32 resultHash, uint64 submittedAt, uint64 gradedAt, uint64 disputedAt, uint64 postedAt, address payee, address feeRecipient, uint16 feeBps, uint16 hireThresholdBps, uint16 gradeThresholdBps, uint16 ruleThresholdBps, uint64 gradeWindow, uint64 disputeWindow, uint64 ruleWindow))",
9
+ "function appealOf(uint256 id) view returns ((uint64 window, uint16 bondBps, uint64 ruledAt, bool pays, bool confident, address appellant, uint128 bond, uint64 appealedAt))",
10
+ "function appealBond(uint256 id) view returns (uint128)",
11
+ "function dispute(uint256 id)",
12
+ "function appeal(uint256 id)",
13
+ ]);
14
+ export function createDisputeChain(network, account) {
15
+ const chain = chainOf(network);
16
+ const transport = brainchainTransport(network);
17
+ const client = createPublicClient({ chain, transport, pollingInterval: 1_000 });
18
+ const wallet = createWalletClient({ account, chain, transport });
19
+ const board = { address: network.contracts.taskBoard, abi: boardAbi };
20
+ const token = { address: network.contracts.bountyToken, abi: erc20Abi };
21
+ async function send(functionName, id) {
22
+ const hash = await wallet.writeContract({ ...board, functionName, args: [id] });
23
+ const receipt = await client.waitForTransactionReceipt({ hash, timeout: RECEIPT_MS });
24
+ if (receipt.status !== "success")
25
+ throw new Error(`Transaction ${hash} reverted`);
26
+ return hash;
27
+ }
28
+ return {
29
+ account: account.address,
30
+ async now() {
31
+ const block = await client.getBlock();
32
+ const wall = BigInt(Math.floor(Date.now() / 1000));
33
+ return block.timestamp > wall ? block.timestamp : wall;
34
+ },
35
+ task: (id) => client.readContract({ ...board, functionName: "get", args: [id] }),
36
+ appealOf: (id) => client.readContract({ ...board, functionName: "appealOf", args: [id] }),
37
+ appealBond: (id) => client.readContract({ ...board, functionName: "appealBond", args: [id] }),
38
+ bountyBalance: () => client.readContract({ ...token, functionName: "balanceOf", args: [account.address] }),
39
+ async approveBounty(amount) {
40
+ const current = await client.readContract({ ...token, functionName: "allowance", args: [account.address, board.address] });
41
+ if (current >= amount)
42
+ return;
43
+ const hash = await wallet.writeContract({ ...token, functionName: "approve", args: [board.address, amount] });
44
+ const receipt = await client.waitForTransactionReceipt({ hash, timeout: RECEIPT_MS });
45
+ if (receipt.status !== "success")
46
+ throw new Error(`Approval ${hash} reverted`);
47
+ },
48
+ dispute: (id) => send("dispute", id),
49
+ appeal: (id) => send("appeal", id),
50
+ };
51
+ }
52
+ const same = (a, b) => a.toLowerCase() === b.toLowerCase();
53
+ const at = (seconds) => new Date(Number(seconds) * 1000).toISOString().replace(/\.000Z$/, "Z");
54
+ /** Disputes a grade on a task the worker delivered, within the dispute window. */
55
+ export async function dispute(network, chain, id, log) {
56
+ const [task, now] = await Promise.all([chain.task(id), chain.now()]);
57
+ if (task.status === 0)
58
+ throw new Error(`There is no task ${id} on ${network.name}.`);
59
+ if (!same(task.payee, chain.account) && !same(task.poster, chain.account)) {
60
+ throw new Error(`Only the poster or the account that delivered task ${id} can dispute it, and ${chain.account} is neither.`);
61
+ }
62
+ if (task.status !== Status.Graded || task.disputed)
63
+ throw new Error(`Task ${id} is not waiting on a dispute: a grade can be disputed once, while it is graded and unsettled.`);
64
+ const closes = task.gradedAt + task.disputeWindow;
65
+ if (now > closes)
66
+ throw new Error(`The dispute window for task ${id} closed at ${at(closes)}.`);
67
+ const tx = await chain.dispute(id);
68
+ log(`Disputed the grade on task ${id} (${task.accepted ? "it accepted the work" : "it rejected the work"}): ${tx}`);
69
+ log(`Jev rules within ${task.ruleWindow / 3600n} hours. A confident ruling decides; otherwise the grade stands.`);
70
+ }
71
+ /** Appeals the ruling on a task, as the side it went against, with the bond the board fixed when the task was posted. */
72
+ export async function appeal(network, chain, id, log) {
73
+ const [task, now] = await Promise.all([chain.task(id), chain.now()]);
74
+ if (task.status === 0)
75
+ throw new Error(`There is no task ${id} on ${network.name}.`);
76
+ if (task.status === Status.Disputed)
77
+ throw new Error(`Task ${id} is still waiting for Jev's ruling. Appeal once it is ruled.`);
78
+ if (task.status === Appealed)
79
+ throw new Error(`Task ${id} is already under appeal; a ruling can be appealed once.`);
80
+ if (task.status !== Ruled)
81
+ throw new Error(`Task ${id} has no ruling open to appeal.`);
82
+ const terms = await chain.appealOf(id);
83
+ const appellant = terms.pays ? task.poster : task.payee;
84
+ if (!same(appellant, chain.account)) {
85
+ throw new Error(`The ruling on task ${id} ${terms.pays ? "pays the model, so only the poster" : "refunds the poster, so only the account that delivered"} can appeal it.`);
86
+ }
87
+ const closes = terms.ruledAt + terms.window;
88
+ if (now > closes)
89
+ throw new Error(`The appeal window for task ${id} closed at ${at(closes)}.`);
90
+ const [bond, balance] = await Promise.all([chain.appealBond(id), chain.bountyBalance()]);
91
+ const amount = `${formatUnits(bond, 6)} ${network.bountySymbol}`;
92
+ if (balance < bond)
93
+ throw new Error(`The appeal bond is ${amount}, and the worker holds ${formatUnits(balance, 6)}. Bridge more with \`npx brainchain-worker bridge --bounty <amount>\`.`);
94
+ await chain.approveBounty(bond);
95
+ const tx = await chain.appeal(id);
96
+ log(`Appealed the ruling on task ${id} with a ${amount} bond: ${tx}`);
97
+ log(`Jev rules again within ${task.ruleWindow / 3600n} hours. The bond comes back unless Jev confidently rules the same way.`);
98
+ }
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: 2141n,
9
+ fromBlock: 2288n,
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: "0x67e0C5F884E1df61AC585B6d158c6E8175956AD1",
34
- staking: "0xd2161eE4194Aaec3c2a7Aa83ccDEeafDf94A72c2",
35
- bonds: "0x85Fd1aE9049ae52DeCdd27b833B16b877e1ce662",
36
- executor: "0x5BFbAf3986E4aFaeb2895535E0bE206F76e5EC63",
37
- registry: "0x7A10D8361917d4446cAEfBd3F71694A438cC6BF8",
38
- log: "0x49019b7ad5B4b688A1bF3c52D9f3a1DF5870F59f",
39
- gate: "0xE59A4d0De46d055196FF73AF605aabBe4B94A70F",
33
+ taskBoard: "0x7A7cC5D35A642CCc268C7B263fF800bc7eBD52A4",
34
+ staking: "0x6aFC3EFB49CF9b14C2F7c6C4f6C0bb3eF0125e2b",
35
+ bonds: "0x2f15B42131544e6763dbECC7C954C7823AAa0D47",
36
+ executor: "0xA3a40A2D46D54dc6e88c08a9d98Ec47785CB2366",
37
+ registry: "0x3e5e23a5fF6548958ab3dDf42c51A59713cE550C",
38
+ log: "0x4E4cB48f3802bD519E0Eed896dF82197B0ecBc09",
39
+ gate: "0x84d01e0869fbCb07c0141E49c390ffdc064D6882",
40
40
  stakeToken: "0xc717546497343911750768335b649b86d0a6eCcf",
41
41
  bountyToken: "0xC4ec72794bF3Ce9ba1DE4B38085464c2eb0cDCf2",
42
42
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainchain-worker",
3
- "version": "0.11.0",
3
+ "version": "0.12.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>",
@@ -28,7 +28,7 @@
28
28
  "prepack": "npm run build"
29
29
  },
30
30
  "dependencies": {
31
- "brainchain-verify": "^0.1.2",
31
+ "brainchain-verify": "^0.3.0",
32
32
  "viem": "^2.0.0"
33
33
  },
34
34
  "devDependencies": {