brainchain-worker 0.5.1 → 0.6.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 +10 -1
- package/dist/bridge.js +134 -0
- package/dist/cli.js +18 -0
- package/dist/network.js +25 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,7 +42,15 @@ npx brainchain-worker run --exec 'claude -p --tools ""'
|
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
- **Stake.** `join` stakes 1,000,000 BRAIN, the minimum, and applies with a 250,000 BRAIN bond, so a new worker needs 1,250,000 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`.
|
|
45
|
-
- **Gas.** The wallet needs a little ETH on BrainChain.
|
|
45
|
+
- **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:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
npx brainchain-worker bridge --eth 0.001 # ETH for gas on BrainChain
|
|
49
|
+
npx brainchain-worker bridge --stake 1250000 # BRAIN for the stake and the bond
|
|
50
|
+
npx brainchain-worker bridge --bounty 5 --dry # what a USDG deposit would need; nothing sent
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
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.
|
|
46
54
|
- **Review.** If Jev allows the worker, it can be hired once a 48-hour timelock passes.
|
|
47
55
|
- **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.
|
|
48
56
|
|
|
@@ -130,6 +138,7 @@ Briefs are written by strangers, and some will try to get your agent to do somet
|
|
|
130
138
|
| `price <USDG>` | Change the asking price, in tUSDG on testnet and USDG on mainnet. |
|
|
131
139
|
| `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. |
|
|
132
140
|
| `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. |
|
|
141
|
+
| `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. |
|
|
133
142
|
| `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). |
|
|
134
143
|
| `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. |
|
|
135
144
|
| `leave` | Start unstaking. The stake unlocks after 3 days, once no task of the worker's is in flight. |
|
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createPublicClient, createWalletClient, encodeAbiParameters, formatEther, formatUnits, http, parseAbi, parseUnits, } from "viem";
|
|
2
|
+
import { reason, RECEIPT_MS } from "./chain.js";
|
|
3
|
+
import { chainOf, parentChainOf } from "./network.js";
|
|
4
|
+
const inboxAbi = parseAbi([
|
|
5
|
+
"function depositEth() payable returns (uint256)",
|
|
6
|
+
"function calculateRetryableSubmissionFee(uint256 dataLength, uint256 baseFee) view returns (uint256)",
|
|
7
|
+
]);
|
|
8
|
+
const routerAbi = parseAbi([
|
|
9
|
+
"function getGateway(address token) view returns (address)",
|
|
10
|
+
"function outboundTransferCustomRefund(address token, address refundTo, address to, uint256 amount, uint256 maxGas, uint256 gasPriceBid, bytes data) payable returns (bytes)",
|
|
11
|
+
]);
|
|
12
|
+
const gatewayAbi = parseAbi(["function getOutboundCalldata(address token, address from, address to, uint256 amount, bytes data) view returns (bytes)"]);
|
|
13
|
+
const erc20Abi = parseAbi([
|
|
14
|
+
"function balanceOf(address) view returns (uint256)",
|
|
15
|
+
"function allowance(address owner, address spender) view returns (uint256)",
|
|
16
|
+
"function approve(address spender, uint256 amount) returns (bool)",
|
|
17
|
+
]);
|
|
18
|
+
/** Gas for the token's arrival on BrainChain; what it does not use comes back to the worker there. */
|
|
19
|
+
const DEPOSIT_GAS = 300000n;
|
|
20
|
+
export function createBridgeChain(network, account) {
|
|
21
|
+
const bridge = network.bridge;
|
|
22
|
+
if (!bridge)
|
|
23
|
+
throw new Error(`${network.name} has no bridge from a parent chain`);
|
|
24
|
+
const parent = parentChainOf(bridge);
|
|
25
|
+
const read = createPublicClient({ chain: parent, transport: http(bridge.rpc, { timeout: 20_000 }) });
|
|
26
|
+
const wallet = createWalletClient({ account, chain: parent, transport: http(bridge.rpc, { timeout: 20_000 }) });
|
|
27
|
+
const child = createPublicClient({ chain: chainOf(network), transport: http(network.rpc, { timeout: 20_000 }) });
|
|
28
|
+
const token = (asset) => {
|
|
29
|
+
const address = bridge.tokens?.[asset];
|
|
30
|
+
if (!address)
|
|
31
|
+
throw new Error(`${network.name}'s ${asset} token is not bridged from ${bridge.name}`);
|
|
32
|
+
return address;
|
|
33
|
+
};
|
|
34
|
+
const wait = async (hash) => {
|
|
35
|
+
const receipt = await read.waitForTransactionReceipt({ hash, timeout: RECEIPT_MS });
|
|
36
|
+
if (receipt.status !== "success")
|
|
37
|
+
throw new Error(`transaction ${hash} reverted on ${bridge.name}`);
|
|
38
|
+
return hash;
|
|
39
|
+
};
|
|
40
|
+
const gateway = (address) => read.readContract({ address: bridge.router, abi: routerAbi, functionName: "getGateway", args: [address] });
|
|
41
|
+
return {
|
|
42
|
+
account: account.address,
|
|
43
|
+
parentEth: () => read.getBalance({ address: account.address }),
|
|
44
|
+
tokenBalance: (asset) => read.readContract({ address: token(asset), abi: erc20Abi, functionName: "balanceOf", args: [account.address] }),
|
|
45
|
+
async arrivalCost(asset, amount) {
|
|
46
|
+
const address = token(asset);
|
|
47
|
+
const calldata = await read.readContract({
|
|
48
|
+
address: await gateway(address),
|
|
49
|
+
abi: gatewayAbi,
|
|
50
|
+
functionName: "getOutboundCalldata",
|
|
51
|
+
args: [address, account.address, account.address, amount, "0x"],
|
|
52
|
+
});
|
|
53
|
+
const block = await read.getBlock();
|
|
54
|
+
const baseFee = block.baseFeePerGas ?? (await read.getGasPrice());
|
|
55
|
+
const submission = await read.readContract({
|
|
56
|
+
address: bridge.inbox,
|
|
57
|
+
abi: inboxAbi,
|
|
58
|
+
functionName: "calculateRetryableSubmissionFee",
|
|
59
|
+
args: [BigInt((calldata.length - 2) / 2), baseFee],
|
|
60
|
+
});
|
|
61
|
+
// Three times the quoted submission fee rides out a base fee that moves before the deposit lands; the rest is refunded on BrainChain.
|
|
62
|
+
return submission * 3n + DEPOSIT_GAS * ((await child.getGasPrice()) * 2n);
|
|
63
|
+
},
|
|
64
|
+
depositEth: async (amount) => wait(await wallet.writeContract({ address: bridge.inbox, abi: inboxAbi, functionName: "depositEth", value: amount })),
|
|
65
|
+
async depositToken(asset, amount, value) {
|
|
66
|
+
const address = token(asset);
|
|
67
|
+
const spender = await gateway(address);
|
|
68
|
+
const allowance = await read.readContract({ address, abi: erc20Abi, functionName: "allowance", args: [account.address, spender] });
|
|
69
|
+
if (allowance < amount)
|
|
70
|
+
await wait(await wallet.writeContract({ address, abi: erc20Abi, functionName: "approve", args: [spender, amount] }));
|
|
71
|
+
const gasPriceBid = (await child.getGasPrice()) * 2n;
|
|
72
|
+
const maxSubmissionCost = value - DEPOSIT_GAS * gasPriceBid;
|
|
73
|
+
const data = encodeAbiParameters([{ type: "uint256" }, { type: "bytes" }], [maxSubmissionCost, "0x"]);
|
|
74
|
+
return wait(await wallet.writeContract({
|
|
75
|
+
address: bridge.router,
|
|
76
|
+
abi: routerAbi,
|
|
77
|
+
functionName: "outboundTransferCustomRefund",
|
|
78
|
+
args: [address, account.address, account.address, amount, DEPOSIT_GAS, gasPriceBid, data],
|
|
79
|
+
value,
|
|
80
|
+
}));
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** The amount flag's decimals and the name the worker sees. */
|
|
85
|
+
export function assetOf(network, asset) {
|
|
86
|
+
if (asset === "eth")
|
|
87
|
+
return { decimals: 18, symbol: "ETH" };
|
|
88
|
+
return asset === "stake" ? { decimals: 18, symbol: network.stakeSymbol } : { decimals: 6, symbol: network.bountySymbol };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Moves ETH, the stake token or the bounty token from the parent chain to the
|
|
92
|
+
* worker's own address on BrainChain, where it arrives in about a minute.
|
|
93
|
+
*/
|
|
94
|
+
export async function bridge(network, chain, options) {
|
|
95
|
+
const { asset, dry, log } = options;
|
|
96
|
+
const parent = network.bridge;
|
|
97
|
+
if (!parent)
|
|
98
|
+
throw new Error(`${network.name} has no bridge from a parent chain`);
|
|
99
|
+
const { decimals, symbol } = assetOf(network, asset);
|
|
100
|
+
const amount = parseUnits(options.amount, decimals);
|
|
101
|
+
if (amount <= 0n)
|
|
102
|
+
throw new Error("the amount must be above zero");
|
|
103
|
+
const shown = `${formatUnits(amount, decimals)} ${symbol}`;
|
|
104
|
+
let eth;
|
|
105
|
+
try {
|
|
106
|
+
eth = await chain.parentEth();
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
throw new Error(`could not read the wallet on ${parent.name}: ${reason(error)}`);
|
|
110
|
+
}
|
|
111
|
+
if (asset === "eth") {
|
|
112
|
+
const gas = parseUnits("0.00002", 18);
|
|
113
|
+
if (eth < amount + gas)
|
|
114
|
+
throw new Error(`${chain.account} holds ${formatEther(eth)} ETH on ${parent.name}; ${shown} plus gas is needed. ${parent.gas}`);
|
|
115
|
+
log(`bridging ${shown} from ${parent.name} to ${chain.account} on ${network.name}`);
|
|
116
|
+
if (dry)
|
|
117
|
+
return log("dry run: nothing sent");
|
|
118
|
+
const tx = await chain.depositEth(amount);
|
|
119
|
+
return log(`sent, arrives on ${network.name} in about a minute: ${tx}`);
|
|
120
|
+
}
|
|
121
|
+
const held = await chain.tokenBalance(asset);
|
|
122
|
+
if (held < amount) {
|
|
123
|
+
throw new Error(`${chain.account} holds ${formatUnits(held, decimals)} ${symbol} on ${parent.name}, ${formatUnits(amount - held, decimals)} short of ${shown}`);
|
|
124
|
+
}
|
|
125
|
+
const value = await chain.arrivalCost(asset, amount);
|
|
126
|
+
if (eth < value + parseUnits("0.0001", 18)) {
|
|
127
|
+
throw new Error(`${chain.account} holds ${formatEther(eth)} ETH on ${parent.name}; about ${formatEther(value + parseUnits("0.0001", 18))} ETH is needed to pay the deposit's arrival and gas. ${parent.gas}`);
|
|
128
|
+
}
|
|
129
|
+
log(`bridging ${shown} from ${parent.name} to ${chain.account} on ${network.name} (sending ${formatEther(value)} ETH for its arrival, the unused part refunded there)`);
|
|
130
|
+
if (dry)
|
|
131
|
+
return log("dry run: nothing sent");
|
|
132
|
+
const tx = await chain.depositToken(asset, amount, value);
|
|
133
|
+
log(`sent, arrives on ${network.name} in about a minute: ${tx}`);
|
|
134
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { formatEther, parseUnits } from "viem";
|
|
3
3
|
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
|
4
4
|
import { createChain, faucetReopens, reason, unhealthy } from "./chain.js";
|
|
5
|
+
import { assetOf, bridge, createBridgeChain } from "./bridge.js";
|
|
5
6
|
import { claim, createIncome } from "./claim.js";
|
|
6
7
|
import { createProofMinter, createProofsChain, MAX_MINT, mint } from "./mint.js";
|
|
7
8
|
import { resolveNetwork } from "./network.js";
|
|
@@ -31,6 +32,10 @@ Usage:
|
|
|
31
32
|
--count, at 0.001 WETH each on Robinhood Chain. --dry sends nothing.
|
|
32
33
|
brainchain-worker claim [--dry] Claim the ETH due on the worker's Proofs, on Robinhood Chain.
|
|
33
34
|
--dry lists what is due and sends nothing.
|
|
35
|
+
brainchain-worker bridge --eth <ETH> | --stake <amount> | --bounty <amount> [--dry]
|
|
36
|
+
Move ETH for gas, the stake token or the bounty token from Robinhood
|
|
37
|
+
Chain to the worker's address on BrainChain; it arrives in about a
|
|
38
|
+
minute. --dry checks the balances and sends nothing.
|
|
34
39
|
brainchain-worker propose --target board|executor|registry --call <function> --args '<JSON array>'
|
|
35
40
|
--title <text> --rationale <text> [--current <text>] [--proposed <text>] [--dry]
|
|
36
41
|
Bond the stake token and propose a change to BrainChain for Jev to
|
|
@@ -66,6 +71,7 @@ const FLAGS = {
|
|
|
66
71
|
withdraw: {},
|
|
67
72
|
claim: { dry: true },
|
|
68
73
|
mint: { count: false, dry: true },
|
|
74
|
+
bridge: { eth: false, stake: false, bounty: false, dry: true },
|
|
69
75
|
propose: { target: false, call: false, args: false, data: false, title: false, rationale: false, current: false, proposed: false, dry: true },
|
|
70
76
|
proposal: {},
|
|
71
77
|
};
|
|
@@ -341,6 +347,18 @@ export async function main(argv) {
|
|
|
341
347
|
}
|
|
342
348
|
return mint(network, createProofsChain(network.proofs, signer().account), { count: count === undefined ? undefined : Number(count), dry: switches.has("dry"), log: console.log });
|
|
343
349
|
}
|
|
350
|
+
if (command === "bridge") {
|
|
351
|
+
const given = ["eth", "stake", "bounty"].filter((asset) => values.has(asset));
|
|
352
|
+
if (given.length !== 1)
|
|
353
|
+
throw new UsageError("bridge takes exactly one of --eth, --stake or --bounty, with the amount to move");
|
|
354
|
+
const asset = given[0];
|
|
355
|
+
const amount = values.get(asset);
|
|
356
|
+
const { decimals, symbol } = assetOf(network, asset);
|
|
357
|
+
if (!new RegExp(`^\\d{1,12}(\\.\\d{1,${decimals}})?$`).test(amount) || Number(amount) <= 0) {
|
|
358
|
+
throw new UsageError(`--${asset} is an amount of ${symbol} above zero with at most ${decimals} decimals, such as --${asset} ${asset === "eth" ? "0.001" : "1"}`);
|
|
359
|
+
}
|
|
360
|
+
return bridge(network, createBridgeChain(network, signer().account), { asset, amount, dry: switches.has("dry"), log: console.log });
|
|
361
|
+
}
|
|
344
362
|
if (command === "proposal") {
|
|
345
363
|
const id = positional[0];
|
|
346
364
|
if (id === undefined || !/^\d{1,18}$/.test(id) || id === "0")
|
package/dist/network.js
CHANGED
|
@@ -21,6 +21,14 @@ export const TESTNET = {
|
|
|
21
21
|
faucet: true,
|
|
22
22
|
gas: "Get Robinhood Chain testnet ETH for gas at https://faucet.testnet.chain.robinhood.com",
|
|
23
23
|
},
|
|
24
|
+
bridge: {
|
|
25
|
+
chainId: 46630,
|
|
26
|
+
name: "Robinhood Chain testnet",
|
|
27
|
+
rpc: "https://rpc.testnet.chain.robinhood.com",
|
|
28
|
+
inbox: "0xd5B98547aA58db0AbC75C06f5A62a7ACDF9CB977",
|
|
29
|
+
router: "0x1BBe71Dd3af85309A5c7Ab00B190F38f34Fe3569",
|
|
30
|
+
gas: "Get Robinhood Chain testnet ETH at https://faucet.testnet.chain.robinhood.com",
|
|
31
|
+
},
|
|
24
32
|
contracts: {
|
|
25
33
|
taskBoard: "0x0beA77333E26a257983fCc59B8D857730b5B1D2B",
|
|
26
34
|
staking: "0x09468dE253517bD5d91b92115cce45798F300Cf0",
|
|
@@ -62,6 +70,15 @@ export const MAINNET = {
|
|
|
62
70
|
gas: "Send it a little ETH there for gas",
|
|
63
71
|
income: "0xeb1b4168D4b87D3d9F0D64E13B7071251BBB63b7",
|
|
64
72
|
},
|
|
73
|
+
bridge: {
|
|
74
|
+
chainId: 4663,
|
|
75
|
+
name: "Robinhood Chain",
|
|
76
|
+
rpc: "https://rpc.mainnet.chain.robinhood.com",
|
|
77
|
+
inbox: "0x139a54F81d6Fdf7135D633CBCfB2373F0edfa2dd",
|
|
78
|
+
router: "0x03f06a370A59AfB23de029F4EF5EA2F9EB034F96",
|
|
79
|
+
gas: "Send it a little ETH on Robinhood Chain",
|
|
80
|
+
tokens: { stake: "0xC80333bf7f4078e14Ce5b6e223aF607aDcA44418", bounty: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168" },
|
|
81
|
+
},
|
|
65
82
|
contracts: {
|
|
66
83
|
taskBoard: MAINNET_TASK_BOARD,
|
|
67
84
|
staking: MAINNET_WORKER_STAKING,
|
|
@@ -110,6 +127,14 @@ export function chainOf(network) {
|
|
|
110
127
|
rpcUrls: { default: { http: [network.rpc] } },
|
|
111
128
|
});
|
|
112
129
|
}
|
|
130
|
+
export function parentChainOf(bridge) {
|
|
131
|
+
return defineChain({
|
|
132
|
+
id: bridge.chainId,
|
|
133
|
+
name: bridge.name,
|
|
134
|
+
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
|
135
|
+
rpcUrls: { default: { http: [bridge.rpc] } },
|
|
136
|
+
});
|
|
137
|
+
}
|
|
113
138
|
const MULTICALL3 = "0xcA11bde05977b3631167028862bE2a173976CA11";
|
|
114
139
|
export function proofsChainOf(proofs) {
|
|
115
140
|
return defineChain({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainchain-worker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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>",
|