brainchain-worker 0.17.0 → 0.18.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
@@ -228,9 +228,9 @@ Earnings come back the same way:
228
228
  BRAINCHAIN_NETWORK=mainnet npx brainchain-worker exit --to-solana <your Solana address>
229
229
  ```
230
230
 
231
- `exit` sends the worker's USDG on BrainChain to the desk, which pays it out on Robinhood Chain within minutes, less a
232
- 0.5% fee, instead of the bridge's seven-day withdrawal. `--to-solana` then moves it on to USDC on Solana through Relay.
233
- Without `--to-solana`, `exit` stops on Robinhood Chain; it works on testnet too, where Relay does not.
231
+ `exit` sends the worker's USDG on BrainChain to the desk, which pays it back within a minute, less a 0.5% fee, instead
232
+ of the bridge's seven-day withdrawal. With `--to-solana` the desk pays it as USDC on Solana through Relay, and the
233
+ worker needs nothing on Robinhood Chain; without it, the desk pays on Robinhood Chain, which also works on testnet.
234
234
 
235
235
  Every Relay move is priced before anything is signed: one that would cost more than 3% of its value, or 0.50 USD on
236
236
  a small move, stops the command and nothing is sent.
package/dist/cli.js CHANGED
@@ -76,11 +76,10 @@ const COMMAND_USAGE = {
76
76
  minute. --eth works on both networks, the tokens on mainnet. --dry
77
77
  checks the balances and sends nothing.`,
78
78
  exit: ` brainchain-worker exit [--amount <USDG>] [--to-solana <address>] [--dry]
79
- Move the worker's USDG on BrainChain to its wallet on Robinhood Chain
80
- within minutes, not the bridge's seven days: the desk pays it out there
81
- less a 0.5% fee. Without --amount, the whole balance. --to-solana then
82
- moves it on to USDC on Solana through Relay (mainnet; needs a little
83
- ETH on Robinhood Chain for gas). --dry checks and sends nothing.`,
79
+ Get the worker's USDG on BrainChain back within a minute, not the
80
+ bridge's seven days: the desk pays it on Robinhood Chain less a 0.5%
81
+ fee, or as USDC on Solana with --to-solana (mainnet). Without --amount,
82
+ the whole balance. --dry checks and sends nothing.`,
84
83
  fund: ` brainchain-worker fund --from-solana --usdc <amount> [--stake] [--gas-usdc <amount>] [--dry]
85
84
  Fund the worker from a Solana wallet: Relay turns its USDC into ETH
86
85
  for gas (2 USDC by default) and, with --stake, the rest into the stake
package/dist/exit.js CHANGED
@@ -1,13 +1,25 @@
1
- // `exit`: the worker's USDG on BrainChain back on Robinhood Chain within minutes instead of the bridge's seven days.
2
- // The desk pays it out on Robinhood Chain and keeps a small fee; `--to-solana` then moves it on to USDC on Solana
3
- // through Relay from the worker's own wallet there.
4
- import { createPublicClient, createWalletClient, erc20Abi, formatUnits, http, parseUnits } from "viem";
1
+ // `exit`: the worker's USDG on BrainChain paid back within a minute instead of the bridge's seven days. The desk pays
2
+ // it on Robinhood Chain, or, with `--to-solana`, as USDC on Solana through Relay: the transfer to the desk carries the
3
+ // Solana address after its arguments, so nothing else is signed and nothing is needed on Robinhood Chain.
4
+ import bs58 from "bs58";
5
+ import { createPublicClient, createWalletClient, encodeFunctionData, erc20Abi, formatUnits, http, parseUnits } from "viem";
5
6
  import { RECEIPT_MS } from "./chain.js";
6
- import { chainOf, parentChainOf } from "./network.js";
7
- import { checkReturn, relayQuote, SOLANA_CHAIN_ID, SOLANA_USDC, waitForRelay } from "./relay.js";
8
- /** A Solana address: 32 to 44 base58 characters. */
7
+ import { chainOf } from "./network.js";
8
+ /** A Solana address: 32 bytes in base58. */
9
9
  export function isSolanaAddress(value) {
10
- return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(value);
10
+ if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(value))
11
+ return false;
12
+ try {
13
+ return bs58.decode(value).length === 32;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ /** A transfer to the desk with the Solana address after its arguments, which the desk reads to pay there. */
20
+ export function exitCalldata(desk, amount, solana) {
21
+ const transfer = encodeFunctionData({ abi: erc20Abi, functionName: "transfer", args: [desk, amount] });
22
+ return solana ? `${transfer}${Buffer.from(bs58.decode(solana)).toString("hex")}` : transfer;
11
23
  }
12
24
  export function exitFee(amount, terms) {
13
25
  const proportional = (amount * BigInt(terms.feeBps)) / 10000n;
@@ -32,35 +44,20 @@ export async function exitRecord(service, tx, fetchImpl = fetch) {
32
44
  return (await response.json()).exits[0];
33
45
  }
34
46
  export function createExitChain(network, account) {
35
- const bridge = network.bridge;
36
- const l3 = { read: createPublicClient({ chain: chainOf(network), transport: http(network.rpc, { timeout: 20_000 }) }) };
37
- const l3Wallet = createWalletClient({ account, chain: chainOf(network), transport: http(network.rpc, { timeout: 20_000 }) });
38
- const parent = bridge ? createPublicClient({ chain: parentChainOf(bridge), transport: http(bridge.rpc, { timeout: 20_000 }) }) : undefined;
39
- const parentWallet = bridge ? createWalletClient({ account, chain: parentChainOf(bridge), transport: http(bridge.rpc, { timeout: 20_000 }) }) : undefined;
47
+ const read = createPublicClient({ chain: chainOf(network), transport: http(network.rpc, { timeout: 20_000 }) });
48
+ const wallet = createWalletClient({ account, chain: chainOf(network), transport: http(network.rpc, { timeout: 20_000 }) });
40
49
  return {
41
50
  account: account.address,
42
- bountyBalance: (token) => l3.read.readContract({ address: token, abi: erc20Abi, functionName: "balanceOf", args: [account.address] }),
43
- async sendToDesk(token, desk, amount) {
44
- const hash = await l3Wallet.writeContract({ address: token, abi: erc20Abi, functionName: "transfer", args: [desk, amount] });
45
- const receipt = await l3.read.waitForTransactionReceipt({ hash, timeout: RECEIPT_MS });
51
+ bountyBalance: (token) => read.readContract({ address: token, abi: erc20Abi, functionName: "balanceOf", args: [account.address] }),
52
+ async sendToDesk(token, desk, amount, solana) {
53
+ const hash = await wallet.sendTransaction({ to: token, data: exitCalldata(desk, amount, solana) });
54
+ const receipt = await read.waitForTransactionReceipt({ hash, timeout: RECEIPT_MS });
46
55
  if (receipt.status !== "success")
47
56
  throw new Error(`the transfer to the desk reverted (${hash})`);
48
57
  return hash;
49
58
  },
50
- parentEth: () => (parent ? parent.getBalance({ address: account.address }) : Promise.resolve(0n)),
51
- async sendParent(item) {
52
- if (!parent || !parentWallet)
53
- throw new Error(`${network.name} has no parent chain`);
54
- const hash = await parentWallet.sendTransaction({ to: item.to, data: item.data, value: BigInt(item.value ?? "0") });
55
- const receipt = await parent.waitForTransactionReceipt({ hash, timeout: RECEIPT_MS });
56
- if (receipt.status !== "success")
57
- throw new Error(`Relay's transaction ${hash} reverted`);
58
- return hash;
59
- },
60
59
  };
61
60
  }
62
- /** Gas the two Relay transactions on Robinhood Chain need, with room to spare. */
63
- export const RELAY_GAS_WEI = 20000000000000n;
64
61
  export async function exit(network, chain, options) {
65
62
  const { log, dry } = options;
66
63
  const fetchImpl = options.fetchImpl ?? fetch;
@@ -89,25 +86,14 @@ export async function exit(network, chain, options) {
89
86
  const fee = exitFee(amount, terms);
90
87
  const net = amount - fee;
91
88
  log(`Exit ${show(amount)} from ${network.name}: ${show(net)} arrives on the parent chain (chain ${terms.payoutChainId}) after a ${show(fee)} fee.`);
92
- if (options.toSolana !== undefined) {
93
- const eth = await chain.parentEth();
94
- if (eth < RELAY_GAS_WEI) {
95
- throw new Error(`Moving it on to Solana needs about 0.00002 ETH on ${network.bridge?.name ?? "the parent chain"} for gas; the wallet holds ${formatUnits(eth, 18)}. \`fund --from-solana\` brings some.`);
96
- }
97
- }
98
- const solanaQuote = (net) => (options.quote ?? relayQuote)({ user: chain.account, recipient: options.toSolana, originChainId: terms.payoutChainId, destinationChainId: SOLANA_CHAIN_ID, originCurrency: terms.payoutToken, destinationCurrency: SOLANA_USDC, amount: net }, fetchImpl);
99
- // Priced before anything leaves BrainChain, so a refused Solana leg never strands the exit halfway.
100
- if (options.toSolana !== undefined) {
101
- const preview = await solanaQuote(net);
102
- checkReturn(preview);
103
- log(`Relay quotes ${preview.outFormatted} USDC on Solana for it.`);
104
- }
89
+ if (options.toSolana !== undefined)
90
+ log(`Paid as USDC on Solana to ${options.toSolana}, less Relay's own fee, unless the move would cost more than 3% (or 0.50 USD); then it is paid on the parent chain.`);
105
91
  if (dry) {
106
92
  log("Dry run, nothing sent.");
107
93
  return;
108
94
  }
109
- const tx = await chain.sendToDesk(terms.token, terms.desk, amount);
110
- log(`Sent to the desk on ${network.name} (${tx}). Waiting for the payout; the desk picks it up within a round, usually a few minutes.`);
95
+ const tx = await chain.sendToDesk(terms.token, terms.desk, amount, options.toSolana);
96
+ log(`Sent to the desk on ${network.name} (${tx}). Waiting for the payout, usually under a minute.`);
111
97
  const end = Date.now() + (options.waitMs ?? 1_800_000);
112
98
  let last = "";
113
99
  let record;
@@ -129,24 +115,9 @@ export async function exit(network, chain, options) {
129
115
  process.exitCode = 1;
130
116
  return;
131
117
  }
132
- log(`Paid on the parent chain: ${show(BigInt(record.net))} (${record.payoutTx}).`);
133
- if (options.toSolana === undefined)
118
+ if (record.solana && record.relayRequest) {
119
+ log(`Paid on Solana: ${show(BigInt(record.net))} as USDC to ${record.solana} (Relay request ${record.relayRequest}).`);
134
120
  return;
135
- const quote = await solanaQuote(BigInt(record.net));
136
- checkReturn(quote);
137
- await sendEvmSteps(quote, chain);
138
- log(`Moving ${show(BigInt(record.net))} to Solana: ${quote.outFormatted} USDC to ${options.toSolana}.`);
139
- const status = await waitForRelay(quote.requestId, 600_000, fetchImpl);
140
- if (status === "success")
141
- log(`Arrived on Solana. Relay request ${quote.requestId}.`);
142
- else {
143
- log(`Relay reports "${status}" for request ${quote.requestId}. A refund comes back to the wallet on the parent chain.`);
144
- process.exitCode = 1;
145
- }
146
- }
147
- async function sendEvmSteps(quote, chain) {
148
- for (const step of quote.steps) {
149
- for (const item of step.items)
150
- await chain.sendParent(item.data);
151
121
  }
122
+ log(`Paid on the parent chain: ${show(BigInt(record.net))} (${record.payoutTx})${record.reason ? `. ${record.reason}` : ""}.`);
152
123
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainchain-worker",
3
- "version": "0.17.0",
3
+ "version": "0.18.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>",