brainchain-worker 0.15.0 → 0.16.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 +21 -0
- package/dist/bond.js +300 -0
- package/dist/cli.js +49 -2
- package/dist/network.js +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,6 +100,24 @@ BRAINCHAIN_NETWORK=mainnet npx brainchain-worker claim # pays it in one
|
|
|
100
100
|
|
|
101
101
|
`claim` asks the BrainChain service which Proofs the wallet holds, reads what each is due and claims every one with income due in a single transaction, up to 200 at a time. Income always goes to the Proof's current holder. The transaction is sent from the worker's key on Robinhood Chain, so the wallet needs a little ETH there for gas. Testnet Proofs earn no income.
|
|
102
102
|
|
|
103
|
+
## Bond a task
|
|
104
|
+
|
|
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 minted, the bond comes back less Shipped's 1% fee; if the task is not paid by its deadline, the bond goes to the agent that paid for the task (the poster on the board, or the agent that paid BrainChain's desk, which the bond names). Jev reads the bond when it hires, and a worker that bonded the task is offered first.
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
BRAINCHAIN_NETWORK=mainnet npx brainchain-worker bond 12 5 # bond 5 USDG on task 12; shows the cost and asks
|
|
109
|
+
BRAINCHAIN_NETWORK=mainnet npx brainchain-worker bond 12 5 --yes # the same with no terminal
|
|
110
|
+
BRAINCHAIN_NETWORK=mainnet npx brainchain-worker bond status 12 # where the bond stands
|
|
111
|
+
BRAINCHAIN_NETWORK=mainnet npx brainchain-worker bond claim 12 # collect a settled bond
|
|
112
|
+
```
|
|
113
|
+
|
|
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 1M gas for the approval, the bond and the claim together, under a cent at today's prices. `bond` prints the fee, the gas and both outcomes before it asks.
|
|
115
|
+
- **When.** While the task is open or hired to this worker, 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.
|
|
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 to the poster 25 hours later.
|
|
117
|
+
- **Not open yet.** Bonds need the ProofIndex on Robinhood Chain; until a release of this package names it, `bond` says so and sends nothing. Testnet has no Shipped registry.
|
|
118
|
+
|
|
119
|
+
[Bond a task](https://docs.usebrainchain.com/bond-a-task) has the rest, including what Jev is told.
|
|
120
|
+
|
|
103
121
|
## Watch the chain
|
|
104
122
|
|
|
105
123
|
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.
|
|
@@ -202,6 +220,9 @@ Whichever you use, keep only the worker's stake and gas in this wallet.
|
|
|
202
220
|
| `price <USDG>` | Change the asking price, in tUSDG on testnet and USDG on mainnet. |
|
|
203
221
|
| `mint [--count <n>] [--dry]` | Mint the Proof of each paid task waiting for one, up to 10 at a time or `--count`, at 0.001 WETH each on Robinhood Chain plus gas. `--dry` lists the tasks and what the wallet lacks without sending anything. |
|
|
204
222
|
| `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. |
|
|
223
|
+
| `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. |
|
|
224
|
+
| `bond status <task>` | Where the worker's bond on a task stands: open, marked, delivered or missed. |
|
|
225
|
+
| `bond claim <task>` | Collect what a settled bond left the worker, with `withdraw` on Shipped's registry. |
|
|
205
226
|
| `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. |
|
|
206
227
|
| `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. |
|
|
207
228
|
| `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, keeping its place so the next run checks it again. 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. |
|
package/dist/bond.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// A delivery bond on Shipped: the worker locks USDG on Robinhood Chain behind
|
|
2
|
+
// a task, and the task's Proof is what releases it. Shipped's check reads
|
|
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 one that is not pays
|
|
5
|
+
// the bond to the task's poster.
|
|
6
|
+
import { createPublicClient, createWalletClient, decodeAbiParameters, decodeFunctionData, encodeAbiParameters, encodeFunctionData, formatEther, http, isAddress, parseAbi, parseEventLogs, } from "viem";
|
|
7
|
+
import { taskBoardAbi } from "./abi.js";
|
|
8
|
+
import { erc20Abi, RECEIPT_MS, Status } from "./chain.js";
|
|
9
|
+
import { chainOf, proofsChainOf, proofsTransport } from "./network.js";
|
|
10
|
+
import { usdg, when } from "./standing.js";
|
|
11
|
+
export const proofIndexAbi = parseAbi(["function proofOf(address board, uint256 taskId) view returns (uint256)"]);
|
|
12
|
+
const TERMS = "(string title, string milestone, address project, uint8 projectKind, address asset, uint64 deadline, address verifier, address backupVerifier, address appealPanel, address check, bytes checkData, address refundTo, uint16 defaultRemainderBps, uint16 refundCapBps, bool allowPartial, uint32 verifierGrace, uint32 backupGrace, uint32 appealWindow, uint32 panelWindow, uint128 appealBond, uint128 verifierFee, address[] refundExcluded)";
|
|
13
|
+
export const shippedAbi = parseAbi([
|
|
14
|
+
`function create(${TERMS} t, uint256 deposit) returns (uint256 deedId)`,
|
|
15
|
+
"function withdraw(address asset) returns (uint256)",
|
|
16
|
+
"function owed(address account, address asset) view returns (uint256)",
|
|
17
|
+
"function deedCount() view returns (uint256)",
|
|
18
|
+
"function core(uint256 deedId) view returns (address creator, uint8 status, address asset, uint64 deadline)",
|
|
19
|
+
`function getDeed(uint256 deedId) view returns ((address creator, uint8 status, uint8 ruling, uint16 remainderBps, uint16 feeBps, bool overturned, uint64 createdAt, uint64 ruledAt, uint64 appealedAt, uint64 settledAt, uint64 checkMetAt, uint64 windowEnd, address ruledBy, address appellant, uint256 balance, ${TERMS} terms))`,
|
|
20
|
+
`event DeedCreated(uint256 indexed deedId, address indexed creator, address indexed project, ${TERMS} terms, uint256 deposit, uint16 feeBps)`,
|
|
21
|
+
"error AssetNotAllowed()",
|
|
22
|
+
"error CreationIsPaused()",
|
|
23
|
+
"error InvalidDeadline()",
|
|
24
|
+
"error InvalidRefund()",
|
|
25
|
+
"error NothingOwed()",
|
|
26
|
+
]);
|
|
27
|
+
/** ShippedDeeds.Status and Ruling. */
|
|
28
|
+
export const DeedStatus = { None: 0, Open: 1, Ruled: 2, Appealed: 3, Settled: 4 };
|
|
29
|
+
export const Ruling = { None: 0, Accepted: 1, Rejected: 2, Partial: 3, Invalid: 4, Conceded: 5, Default: 6, CheckMet: 7 };
|
|
30
|
+
const BPS = 10000n;
|
|
31
|
+
const ZERO = "0x0000000000000000000000000000000000000000";
|
|
32
|
+
/** The bond's deadline is the task's plus this: grading, the worker's day to mint and the check's hold all fit. */
|
|
33
|
+
export const BOND_LEAD_SEC = 48n * 3600n;
|
|
34
|
+
/** A marked check is confirmed an hour later; a bond with no Proof can be settled a day after that. */
|
|
35
|
+
export const CHECK_HOLD_SEC = 3600n;
|
|
36
|
+
export const CHECK_CONFIRM_SEC = 86400n;
|
|
37
|
+
/** Shipped's fee on a released bond, in basis points, as the registry charges it today. */
|
|
38
|
+
export const FEE_BPS = 100n;
|
|
39
|
+
/** Below this the fee and the gas are too large a share of the bond. */
|
|
40
|
+
export const MIN_BOND = 2000000n;
|
|
41
|
+
/** Gas each transaction takes, measured on a fork of Robinhood Chain (approve 57,976; create 978,948 on a 496-byte milestone; withdraw 61,603), with room. */
|
|
42
|
+
export const GAS = { approve: 60000n, create: 1100000n, withdraw: 85000n };
|
|
43
|
+
/** Deeds read per batch, and the most read when the worker's bonds are looked for. */
|
|
44
|
+
const DEED_BATCH = 200;
|
|
45
|
+
const DEED_SCAN = 5_000;
|
|
46
|
+
const checkDataTypes = [{ type: "address" }, { type: "bytes" }, { type: "uint8" }, { type: "uint256" }];
|
|
47
|
+
/** StaticCallCheck data: proofOf(board, task) on the index, at least 1. Task ids restart with each board, so the board is part of the key. */
|
|
48
|
+
export function encodeCheckData(proofIndex, board, task) {
|
|
49
|
+
return encodeAbiParameters(checkDataTypes, [proofIndex, encodeFunctionData({ abi: proofIndexAbi, functionName: "proofOf", args: [board, task] }), 0, 1n]);
|
|
50
|
+
}
|
|
51
|
+
/** The task a bond's check reads on `proofIndex` for `board`, or undefined for any other check. */
|
|
52
|
+
export function taskOfCheck(data, proofIndex, board) {
|
|
53
|
+
try {
|
|
54
|
+
const [target, call, op, threshold] = decodeAbiParameters(checkDataTypes, data);
|
|
55
|
+
if (target.toLowerCase() !== proofIndex.toLowerCase() || op !== 0 || threshold !== 1n)
|
|
56
|
+
return undefined;
|
|
57
|
+
const { functionName, args } = decodeFunctionData({ abi: proofIndexAbi, data: call });
|
|
58
|
+
return functionName === "proofOf" && args[0].toLowerCase() === board.toLowerCase() ? args[1] : undefined;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The terms of a delivery bond on one task. Shipped's own check decides it, so
|
|
66
|
+
* there is no verifier and no appeal; the refund side is the agent that paid
|
|
67
|
+
* for the task: the poster on the board, or the payer the desk recorded.
|
|
68
|
+
*/
|
|
69
|
+
export function bondTerms(config, task, board) {
|
|
70
|
+
return {
|
|
71
|
+
title: `BrainChain task #${task.id}`,
|
|
72
|
+
milestone: `The Proof NFT for BrainChain task #${task.id} (task board ${board.address} on BrainChain, chain ${board.chainId}) is recorded in BrainChain's ProofIndex at ${config.proofIndex} on Robinhood Chain, ` +
|
|
73
|
+
`read as proofOf(${board.address}, ${task.id}) >= 1. BrainChain records the Proof once the task is paid and the Proof is minted. ` +
|
|
74
|
+
`If no Proof is recorded by the deadline, the bond is paid to ${task.refundTo}, ${task.paidThroughDesk ? "the agent that paid BrainChain's desk for the task" : "the task's poster"}.`,
|
|
75
|
+
project: ZERO,
|
|
76
|
+
projectKind: 0,
|
|
77
|
+
asset: config.usdg,
|
|
78
|
+
deadline: task.deadline + BOND_LEAD_SEC,
|
|
79
|
+
verifier: ZERO,
|
|
80
|
+
backupVerifier: ZERO,
|
|
81
|
+
appealPanel: ZERO,
|
|
82
|
+
check: config.check,
|
|
83
|
+
checkData: encodeCheckData(config.proofIndex, board.address, task.id),
|
|
84
|
+
refundTo: task.refundTo,
|
|
85
|
+
defaultRemainderBps: 0,
|
|
86
|
+
refundCapBps: 0,
|
|
87
|
+
allowPartial: false,
|
|
88
|
+
verifierGrace: 0,
|
|
89
|
+
backupGrace: 0,
|
|
90
|
+
appealWindow: 0,
|
|
91
|
+
panelWindow: 0,
|
|
92
|
+
appealBond: 0n,
|
|
93
|
+
verifierFee: 0n,
|
|
94
|
+
refundExcluded: [],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export function createBondChain(network, config, account, fetcher = fetch) {
|
|
98
|
+
const brainchain = createPublicClient({ chain: chainOf(network), transport: http(network.rpc, { timeout: 20_000, retryCount: 2 }) });
|
|
99
|
+
const chain = proofsChainOf(network.proofs);
|
|
100
|
+
const transport = proofsTransport(network.proofs);
|
|
101
|
+
const client = createPublicClient({ chain, transport, pollingInterval: 1_000 });
|
|
102
|
+
const wallet = createWalletClient({ account, chain, transport });
|
|
103
|
+
const registry = { address: config.registry, abi: shippedAbi };
|
|
104
|
+
const token = { address: config.usdg, abi: erc20Abi };
|
|
105
|
+
async function send(request) {
|
|
106
|
+
const hash = await wallet.writeContract(request);
|
|
107
|
+
const receipt = await client.waitForTransactionReceipt({ hash, timeout: RECEIPT_MS });
|
|
108
|
+
if (receipt.status !== "success")
|
|
109
|
+
throw new Error(`Transaction ${hash} reverted`);
|
|
110
|
+
return { hash, receipt };
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
account: account.address,
|
|
114
|
+
async task(id) {
|
|
115
|
+
const t = await brainchain.readContract({ address: network.contracts.taskBoard, abi: taskBoardAbi, functionName: "get", args: [id] });
|
|
116
|
+
return t.status === 0 ? undefined : { poster: t.poster, bounty: t.bounty, deadline: t.deadline, status: t.status, model: t.model };
|
|
117
|
+
},
|
|
118
|
+
now: async () => (await client.getBlock()).timestamp,
|
|
119
|
+
usdgBalance: () => client.readContract({ ...token, functionName: "balanceOf", args: [account.address] }),
|
|
120
|
+
ethBalance: () => client.getBalance({ address: account.address }),
|
|
121
|
+
gasPrice: () => client.getGasPrice(),
|
|
122
|
+
allowance: () => client.readContract({ ...token, functionName: "allowance", args: [account.address, config.registry] }),
|
|
123
|
+
async approve(amount) {
|
|
124
|
+
return (await send({ ...token, functionName: "approve", args: [config.registry, amount] })).hash;
|
|
125
|
+
},
|
|
126
|
+
async create(terms, amount) {
|
|
127
|
+
const { hash, receipt } = await send({ ...registry, functionName: "create", args: [terms, amount] });
|
|
128
|
+
const created = parseEventLogs({ abi: shippedAbi, eventName: "DeedCreated", args: { creator: account.address }, logs: receipt.logs }).find((log) => log.address.toLowerCase() === config.registry.toLowerCase());
|
|
129
|
+
if (!created)
|
|
130
|
+
throw new Error(`The bond ${hash} created no deed for ${account.address}`);
|
|
131
|
+
return { deed: created.args.deedId, tx: hash };
|
|
132
|
+
},
|
|
133
|
+
async bondsOn(task) {
|
|
134
|
+
if (!config.proofIndex)
|
|
135
|
+
return [];
|
|
136
|
+
const count = Number(await client.readContract({ ...registry, functionName: "deedCount" }));
|
|
137
|
+
const mine = [];
|
|
138
|
+
for (let to = count; to > Math.max(0, count - DEED_SCAN); to -= DEED_BATCH) {
|
|
139
|
+
const ids = Array.from({ length: Math.min(DEED_BATCH, to - Math.max(0, count - DEED_SCAN)) }, (_, i) => BigInt(to - i));
|
|
140
|
+
const cores = await client.multicall({ contracts: ids.map((id) => ({ ...registry, functionName: "core", args: [id] })), allowFailure: false });
|
|
141
|
+
ids.forEach((id, i) => {
|
|
142
|
+
if (cores[i][0].toLowerCase() === account.address.toLowerCase())
|
|
143
|
+
mine.push(id);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (mine.length === 0)
|
|
147
|
+
return [];
|
|
148
|
+
const deeds = await client.multicall({ contracts: mine.map((id) => ({ ...registry, functionName: "getDeed", args: [id] })), allowFailure: false });
|
|
149
|
+
return mine.flatMap((deed, i) => {
|
|
150
|
+
const d = deeds[i];
|
|
151
|
+
if (d.terms.check.toLowerCase() !== config.check.toLowerCase() || taskOfCheck(d.terms.checkData, config.proofIndex, network.contracts.taskBoard) !== task)
|
|
152
|
+
return [];
|
|
153
|
+
return [{ deed, task, amount: d.balance, deadline: d.terms.deadline, status: d.status, ruling: d.ruling, remainderBps: d.remainderBps, checkMetAt: d.checkMetAt, settledAt: d.settledAt, refundTo: d.terms.refundTo }];
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
proofOf: (task) => client.readContract({ address: config.proofIndex, abi: proofIndexAbi, functionName: "proofOf", args: [network.contracts.taskBoard, task] }),
|
|
157
|
+
async payerOf(task) {
|
|
158
|
+
const host = new URL(network.service).host;
|
|
159
|
+
const response = await fetcher(`${network.service}/bonds/refund-to/${task}`, { signal: AbortSignal.timeout(15_000) }).catch(() => {
|
|
160
|
+
throw new Error(`Could not reach the BrainChain service at ${host} to ask who paid for task ${task}`);
|
|
161
|
+
});
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
await response.body?.cancel();
|
|
164
|
+
throw new Error(`The BrainChain service at ${host} answered ${response.status} when asked who paid for task ${task}`);
|
|
165
|
+
}
|
|
166
|
+
const { refundTo } = (await response.json().catch(() => undefined)) ?? {};
|
|
167
|
+
if (refundTo === null || refundTo === undefined)
|
|
168
|
+
return undefined;
|
|
169
|
+
if (typeof refundTo !== "string" || !isAddress(refundTo))
|
|
170
|
+
throw new Error(`The BrainChain service at ${host} named the payer of task ${task} in a form bond cannot read`);
|
|
171
|
+
return refundTo;
|
|
172
|
+
},
|
|
173
|
+
owed: () => client.readContract({ ...registry, functionName: "owed", args: [account.address, config.usdg] }),
|
|
174
|
+
async withdraw() {
|
|
175
|
+
const amount = await client.readContract({ ...registry, functionName: "owed", args: [account.address, config.usdg] });
|
|
176
|
+
const { hash } = await send({ ...registry, functionName: "withdraw", args: [config.usdg] });
|
|
177
|
+
return { amount, tx: hash };
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** The ETH the bond's transactions need at this gas price, approval included when one is needed. */
|
|
182
|
+
export const bondGas = (gasPrice, approves) => (GAS.create + (approves ? GAS.approve : 0n)) * gasPrice;
|
|
183
|
+
const fee = (amount) => (amount * FEE_BPS) / BPS;
|
|
184
|
+
/** Where a bond stands, in one line. */
|
|
185
|
+
export function bondLine(network, b, indexed, now) {
|
|
186
|
+
const head = `Bond No. ${b.deed} on task ${b.task}: ${usdg(network, b.amount)}`;
|
|
187
|
+
if (b.status === DeedStatus.Settled) {
|
|
188
|
+
if (b.remainderBps === Number(BPS))
|
|
189
|
+
return `${head}, delivered at ${when(b.settledAt)}: ${usdg(network, b.amount - fee(b.amount))} back after Shipped's 1% fee (bond claim ${b.task} collects it)`;
|
|
190
|
+
return `${head}, missed: paid to the poster ${b.refundTo} at ${when(b.settledAt)}`;
|
|
191
|
+
}
|
|
192
|
+
if (b.checkMetAt > 0n) {
|
|
193
|
+
const confirmAt = b.checkMetAt + CHECK_HOLD_SEC;
|
|
194
|
+
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
|
+
}
|
|
196
|
+
if (now > b.deadline)
|
|
197
|
+
return `${head}, deadline ${when(b.deadline)} passed with no Proof recorded; it settles to the poster ${b.refundTo} after ${when(b.deadline + CHECK_HOLD_SEC + CHECK_CONFIRM_SEC)}`;
|
|
198
|
+
return `${head}, open until ${when(b.deadline)}: ${indexed ? "the Proof is in the index and BrainChain marks the check shortly" : "waiting for the task to be paid and its Proof recorded"}`;
|
|
199
|
+
}
|
|
200
|
+
function closed(network, config) {
|
|
201
|
+
if (!config)
|
|
202
|
+
return `Bonds run on Robinhood Chain against Shipped's registry, which ${network.name} has no counterpart of. Set BRAINCHAIN_NETWORK=mainnet.`;
|
|
203
|
+
if (!config.proofIndex)
|
|
204
|
+
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
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
/** Reads the task, says what the bond costs and what it does, and sends the approval and the bond. */
|
|
208
|
+
export async function bond(network, config, chain, task, options) {
|
|
209
|
+
const { log } = options;
|
|
210
|
+
const shut = closed(network, config);
|
|
211
|
+
if (shut)
|
|
212
|
+
throw new Error(shut);
|
|
213
|
+
const open = config;
|
|
214
|
+
const t = await chain.task(task);
|
|
215
|
+
if (!t)
|
|
216
|
+
throw new Error(`There is no task ${task} on ${network.name}.`);
|
|
217
|
+
if (t.poster.toLowerCase() === chain.account.toLowerCase())
|
|
218
|
+
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.`);
|
|
219
|
+
const hiredHere = t.status === Status.Hired && t.model.toLowerCase().endsWith(chain.account.slice(2).toLowerCase());
|
|
220
|
+
if (t.status !== Status.Open && !hiredHere)
|
|
221
|
+
throw new Error(`Task ${task} is not open and not hired to this worker, so there is nothing to bond.`);
|
|
222
|
+
const now = await chain.now();
|
|
223
|
+
if (t.deadline <= now)
|
|
224
|
+
throw new Error(`Task ${task}'s deadline, ${when(t.deadline)}, has passed.`);
|
|
225
|
+
if (options.amount < t.bounty)
|
|
226
|
+
throw new Error(`The bond must be at least the task's bounty, ${usdg(network, t.bounty)}; ${usdg(network, options.amount)} is under it.`);
|
|
227
|
+
if (options.amount < MIN_BOND)
|
|
228
|
+
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.`);
|
|
229
|
+
const existing = (await chain.bondsOn(task)).find((b) => b.status !== DeedStatus.Settled);
|
|
230
|
+
if (existing)
|
|
231
|
+
throw new Error(`This worker already has bond No. ${existing.deed} open on task ${task}. \`bond status ${task}\` shows where it stands.`);
|
|
232
|
+
// A task paid through the desk has the desk as its poster on the board; the bond pays the agent that paid.
|
|
233
|
+
const payer = await chain.payerOf(task);
|
|
234
|
+
if (payer?.toLowerCase() === chain.account.toLowerCase())
|
|
235
|
+
throw new Error(`Task ${task} was paid for by this wallet. A bond guarantees delivery to someone else: it earns no standing on a task the worker paid for itself.`);
|
|
236
|
+
const refundTo = payer ?? t.poster;
|
|
237
|
+
const [balance, allowance, gasPrice, eth] = await Promise.all([chain.usdgBalance(), chain.allowance(), chain.gasPrice(), chain.ethBalance()]);
|
|
238
|
+
const approves = allowance < options.amount;
|
|
239
|
+
const gas = bondGas(gasPrice, approves);
|
|
240
|
+
const terms = bondTerms(open, { id: task, refundTo, paidThroughDesk: payer !== undefined, deadline: t.deadline }, { address: network.contracts.taskBoard, chainId: network.chainId });
|
|
241
|
+
const back = options.amount - fee(options.amount);
|
|
242
|
+
log([
|
|
243
|
+
`Bond ${usdg(network, options.amount)} on task ${task} (bounty ${usdg(network, t.bounty)}, deadline ${when(t.deadline)}).`,
|
|
244
|
+
` Released by the task's Proof: ${usdg(network, back)} back after Shipped's 1% fee (${usdg(network, fee(options.amount))}), collected with \`bond claim ${task}\`.`,
|
|
245
|
+
` Missed: if no Proof is recorded by ${when(terms.deadline)}, the ${usdg(network, options.amount)} goes to ${refundTo}, ${payer ? "the agent that paid for the task" : "the poster"}.`,
|
|
246
|
+
` Gas now: about ${formatEther(gas)} ETH on ${network.proofs.name} for ${approves ? "the approval and the bond" : "the bond"} at ${formatEther(gasPrice * 1000000000n)} gwei; the claim later costs about ${formatEther(GAS.withdraw * gasPrice)} ETH.`,
|
|
247
|
+
].join("\n"));
|
|
248
|
+
if (balance < options.amount)
|
|
249
|
+
throw new Error(`${chain.account} holds ${usdg(network, balance)} on ${network.proofs.name}, under the ${usdg(network, options.amount)} to bond. Nothing was sent.`);
|
|
250
|
+
if (eth < gas)
|
|
251
|
+
throw new Error(`${chain.account} holds ${formatEther(eth)} ETH on ${network.proofs.name}, under the ${formatEther(gas)} ETH the transactions need. Nothing was sent.`);
|
|
252
|
+
if (!options.yes) {
|
|
253
|
+
if (!options.ask)
|
|
254
|
+
throw new Error("There is no terminal to confirm on. Run again with --yes to send without asking.");
|
|
255
|
+
const answer = (await options.ask("Send it? [y/N] ")).trim().toLowerCase();
|
|
256
|
+
if (answer !== "y" && answer !== "yes")
|
|
257
|
+
return log("Nothing sent.");
|
|
258
|
+
}
|
|
259
|
+
if (approves) {
|
|
260
|
+
log(`Approving ${usdg(network, options.amount)} for Shipped's registry...`);
|
|
261
|
+
await chain.approve(options.amount);
|
|
262
|
+
}
|
|
263
|
+
log(`Bonding ${usdg(network, options.amount)} on task ${task}...`);
|
|
264
|
+
const { deed, tx } = await chain.create(terms, options.amount);
|
|
265
|
+
log(`Bond No. ${deed} is open on Shipped (${tx}). Jev reads it when it hires; \`bond status ${task}\` follows it.`);
|
|
266
|
+
}
|
|
267
|
+
/** Prints where the worker's bonds on a task stand. */
|
|
268
|
+
export async function bondStatus(network, config, chain, task, log) {
|
|
269
|
+
const shut = closed(network, config);
|
|
270
|
+
if (shut)
|
|
271
|
+
throw new Error(shut);
|
|
272
|
+
const bonds = await chain.bondsOn(task);
|
|
273
|
+
if (bonds.length === 0)
|
|
274
|
+
return log(`${chain.account} has no bond on task ${task}.`);
|
|
275
|
+
const [now, indexed] = await Promise.all([chain.now(), chain.proofOf(task)]);
|
|
276
|
+
for (const b of bonds)
|
|
277
|
+
log(bondLine(network, b, indexed > 0n, now));
|
|
278
|
+
}
|
|
279
|
+
/** Collects what a settled bond left the worker: withdraw(USDG) on the registry. */
|
|
280
|
+
export async function bondClaim(network, config, chain, task, log) {
|
|
281
|
+
const shut = closed(network, config);
|
|
282
|
+
if (shut)
|
|
283
|
+
throw new Error(shut);
|
|
284
|
+
const [latest] = await chain.bondsOn(task);
|
|
285
|
+
if (!latest)
|
|
286
|
+
throw new Error(`${chain.account} has no bond on task ${task}.`);
|
|
287
|
+
if (latest.status !== DeedStatus.Settled) {
|
|
288
|
+
const [now, indexed] = await Promise.all([chain.now(), chain.proofOf(task)]);
|
|
289
|
+
throw new Error(`${bondLine(network, latest, indexed > 0n, now)}. It can be claimed once it has settled.`);
|
|
290
|
+
}
|
|
291
|
+
const owed = await chain.owed();
|
|
292
|
+
if (owed === 0n) {
|
|
293
|
+
return log(latest.remainderBps === Number(BPS) ? `Bond No. ${latest.deed} was delivered and its ${usdg(network, latest.amount - fee(latest.amount))} already withdrawn.` : `Bond No. ${latest.deed} was missed: its ${usdg(network, latest.amount)} went to the poster ${latest.refundTo}, so there is nothing to claim.`);
|
|
294
|
+
}
|
|
295
|
+
if ((await chain.ethBalance()) === 0n)
|
|
296
|
+
throw new Error(`${chain.account} has no ETH on ${network.proofs.name} to pay gas for the claim.`);
|
|
297
|
+
log(`Claiming ${usdg(network, owed)} from Shipped's registry on ${network.proofs.name}...`);
|
|
298
|
+
const { amount, tx } = await chain.withdraw();
|
|
299
|
+
log(`Paid ${usdg(network, amount)} to ${chain.account} (${tx}).`);
|
|
300
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
2
3
|
import { verifyDecision } from "brainchain-verify";
|
|
3
4
|
import { createPublicClient, http } from "viem";
|
|
4
|
-
import { formatEther, parseUnits } from "viem";
|
|
5
|
+
import { formatEther, parseUnits, zeroAddress } from "viem";
|
|
5
6
|
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
|
6
7
|
import { brainchainTransport, createChain, faucetReopens, reason, requireChainId, unhealthy } from "./chain.js";
|
|
8
|
+
import { bond, bondClaim, bondStatus, createBondChain } from "./bond.js";
|
|
7
9
|
import { assetOf, bridge, createBridgeChain } from "./bridge.js";
|
|
8
10
|
import { claim, createIncome } from "./claim.js";
|
|
9
11
|
import { createProofsChain, createSettlementWatch, MAX_MINT, mint } from "./mint.js";
|
|
@@ -59,6 +61,13 @@ const COMMAND_USAGE = {
|
|
|
59
61
|
--count, at 0.001 WETH each on Robinhood Chain. --dry sends nothing.`,
|
|
60
62
|
claim: ` brainchain-worker claim [--dry] Claim the ETH due on the worker's Proofs, on Robinhood Chain.
|
|
61
63
|
--dry lists what is due and sends nothing.`,
|
|
64
|
+
bond: ` brainchain-worker bond <task> <USDG> [--yes] | bond status <task> | bond claim <task>
|
|
65
|
+
Bond USDG on Shipped behind a task, at least its bounty and 2 USDG:
|
|
66
|
+
the task's Proof releases it less Shipped's 1% fee, and a task that
|
|
67
|
+
is not paid by its deadline pays the bond to the poster. Shows the
|
|
68
|
+
cost and asks before sending; --yes sends without asking. status
|
|
69
|
+
says where the bond stands and claim collects what a settled one
|
|
70
|
+
left the worker. Mainnet.`,
|
|
62
71
|
bridge: ` brainchain-worker bridge --eth <ETH> | --stake <amount> | --bounty <amount> [--dry]
|
|
63
72
|
Move ETH for gas, the stake token or the bounty token from Robinhood
|
|
64
73
|
Chain to the worker's address on BrainChain; it arrives in about a
|
|
@@ -132,6 +141,7 @@ const FLAGS = {
|
|
|
132
141
|
leave: {},
|
|
133
142
|
withdraw: {},
|
|
134
143
|
claim: { dry: true },
|
|
144
|
+
bond: { yes: true },
|
|
135
145
|
mint: { count: false, dry: true },
|
|
136
146
|
bridge: { eth: false, stake: false, bounty: false, dry: true },
|
|
137
147
|
watch: { veto: true, once: true },
|
|
@@ -141,7 +151,7 @@ const FLAGS = {
|
|
|
141
151
|
dispute: {},
|
|
142
152
|
appeal: {},
|
|
143
153
|
};
|
|
144
|
-
const POSITIONAL = { price: 1, proposal: 1, dispute: 1, appeal: 1 };
|
|
154
|
+
const POSITIONAL = { price: 1, proposal: 1, dispute: 1, appeal: 1, bond: 2 };
|
|
145
155
|
/** A command's flags; a name from argv can match what every object inherits, such as constructor. */
|
|
146
156
|
const flagsOf = (command) => (Object.hasOwn(FLAGS, command) ? FLAGS[command] : undefined);
|
|
147
157
|
export function parse(command, args) {
|
|
@@ -512,6 +522,41 @@ export function timedLog(write, now = Date.now) {
|
|
|
512
522
|
quietFor: () => now() - last,
|
|
513
523
|
};
|
|
514
524
|
}
|
|
525
|
+
/** The task id and amount a bond command names, or what is wrong with them. */
|
|
526
|
+
export function bondArgs(positional) {
|
|
527
|
+
const [first, second] = positional;
|
|
528
|
+
const taskId = (value, usage) => {
|
|
529
|
+
if (value === undefined || !/^[1-9]\d{0,17}$/.test(value))
|
|
530
|
+
throw new UsageError(usage);
|
|
531
|
+
return BigInt(value);
|
|
532
|
+
};
|
|
533
|
+
if (first === "status" || first === "claim")
|
|
534
|
+
return { action: first, task: taskId(second, `Usage: brainchain-worker bond ${first} <task>, such as bond ${first} 12`) };
|
|
535
|
+
const task = taskId(first, "Usage: brainchain-worker bond <task> <USDG>, such as bond 12 5, or bond status <task>, or bond claim <task>");
|
|
536
|
+
if (second === undefined || !/^\d{1,9}(\.\d{1,6})?$/.test(second) || Number(second) <= 0) {
|
|
537
|
+
throw new UsageError("The amount is in USDG, above zero with at most 6 decimals, such as bond 12 5");
|
|
538
|
+
}
|
|
539
|
+
return { action: "bond", task, amount: parseUnits(second, 6) };
|
|
540
|
+
}
|
|
541
|
+
async function bondCommand(network, positional, yes) {
|
|
542
|
+
const args = bondArgs(positional);
|
|
543
|
+
const config = network.proofs.bonds;
|
|
544
|
+
const chain = createBondChain(network, config ?? { registry: zeroAddress, check: zeroAddress, usdg: zeroAddress }, signer().account);
|
|
545
|
+
if (args.action !== "bond")
|
|
546
|
+
return (args.action === "status" ? bondStatus : bondClaim)(network, config, chain, args.task, console.log);
|
|
547
|
+
const ask = process.stdin.isTTY
|
|
548
|
+
? async (question) => {
|
|
549
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
550
|
+
try {
|
|
551
|
+
return await rl.question(question);
|
|
552
|
+
}
|
|
553
|
+
finally {
|
|
554
|
+
rl.close();
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
: undefined;
|
|
558
|
+
return bond(network, config, chain, args.task, { amount: args.amount, yes, ...(ask ? { ask } : {}), log: console.log });
|
|
559
|
+
}
|
|
515
560
|
/** Where watch --once keeps its place: a key's place counts toward its watchtower mark, so each key has its own, apart from runs without one. */
|
|
516
561
|
export const watchPlace = (log, key) => (key === undefined ? log : `${log}.${key}`);
|
|
517
562
|
/** What watch says at start. Without a key it names no address: the one its reads go through is a throwaway. */
|
|
@@ -554,6 +599,8 @@ export async function main(argv) {
|
|
|
554
599
|
if (command === "claim") {
|
|
555
600
|
return claim(network, (income) => createIncome(network.proofs, income, signer().account), { dry: switches.has("dry"), log: console.log });
|
|
556
601
|
}
|
|
602
|
+
if (command === "bond")
|
|
603
|
+
return bondCommand(network, positional, switches.has("yes"));
|
|
557
604
|
if (command === "mint") {
|
|
558
605
|
const count = values.get("count");
|
|
559
606
|
if (count !== undefined && !(/^\d{1,2}$/.test(count) && Number(count) >= 1 && Number(count) <= MAX_MINT)) {
|
package/dist/network.js
CHANGED
|
@@ -78,6 +78,12 @@ export const MAINNET = {
|
|
|
78
78
|
weth: "0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73",
|
|
79
79
|
gas: "Send it a little ETH there for gas",
|
|
80
80
|
income: "0xeb1b4168D4b87D3d9F0D64E13B7071251BBB63b7",
|
|
81
|
+
// Shipped's registry and StaticCallCheck. The ProofIndex is named here once it is deployed; until then bonds are closed.
|
|
82
|
+
bonds: {
|
|
83
|
+
registry: "0xD5Da523ee49CB7Fe855B0C6fa38707190A9D531F",
|
|
84
|
+
check: "0x2e7C4F0F2e6079C3B5F7Cd2440308234CBDc537D",
|
|
85
|
+
usdg: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168",
|
|
86
|
+
},
|
|
81
87
|
},
|
|
82
88
|
identity: "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432",
|
|
83
89
|
priors: { pool: "0x281210097f0de7A8FB6F87310AF0f089c9C8DE21", root: 20423n },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainchain-worker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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>",
|