bridge.js

raw

#!/usr/bin/env node
/*
 * Bridge a small amount of ETH from Base to Optimism via Relay, so we can pay the
 * Farcaster registration fee (the ID contracts live on Optimism, funds arrived on Base).
 *
 *   node bridge.js 0.0008        # amount of ETH to send from Base
 *
 * Fetches a fresh quote and sends it immediately — quotes go stale.
 */
const { JsonRpcProvider, Wallet, parseEther, formatEther } = require("ethers");
const fs = require("fs");

const ADDR = "0x1C7afa67130ee637765a8281E83342E307409D57";
const AMT = process.argv[2] || "0.0008";

(async () => {
  const keys = JSON.parse(fs.readFileSync(__dirname + "/keys.json", "utf8"));
  const base = new JsonRpcProvider("https://mainnet.base.org");
  const op = new JsonRpcProvider("https://mainnet.optimism.io");
  const w = new Wallet(keys.privateKey, base);

  const before = { base: await base.getBalance(ADDR), op: await op.getBalance(ADDR) };
  console.log("before  base:", formatEther(before.base), " op:", formatEther(before.op));

  const wei = parseEther(AMT).toString();
  const q = await fetch("https://api.relay.link/quote", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      user: ADDR, recipient: ADDR,
      originChainId: 8453, destinationChainId: 10,
      originCurrency: "0x0000000000000000000000000000000000000000",
      destinationCurrency: "0x0000000000000000000000000000000000000000",
      amount: wei, tradeType: "EXACT_INPUT",
    }),
  }).then(r => r.json());

  const step = (q.steps || []).find(s => s.kind === "transaction");
  if (!step) throw new Error("no transaction step in quote: " + JSON.stringify(q).slice(0, 300));
  const tx = step.items[0].data;
  const out = q.details?.currencyOut;
  console.log(`quote: send ${AMT} ETH on Base -> receive ${out?.amountFormatted} ETH on Optimism`);
  console.log("to:", tx.to);

  if (tx.chainId !== 8453) throw new Error("quote is not for Base; refusing");

  const sent = await w.sendTransaction({
    to: tx.to, data: tx.data, value: BigInt(tx.value),
    gasLimit: BigInt(Math.ceil(Number(tx.gas || 60000) * 1.5)),
  });
  console.log("sent on Base:", sent.hash, "— waiting…");
  const rc = await sent.wait();
  console.log("confirmed in block", rc.blockNumber);

  // relayer fills on the destination side; usually seconds
  process.stdout.write("waiting for Optimism side");
  for (let i = 0; i < 40; i++) {
    const bal = await op.getBalance(ADDR);
    if (bal > before.op) {
      console.log(`\nARRIVED on Optimism: ${formatEther(bal)} ETH`);
      return;
    }
    process.stdout.write(".");
    await new Promise(r => setTimeout(r, 5000));
  }
  console.log("\nnot yet visible on Optimism — check later, relayers can lag.");
})().catch(e => { console.error("ERROR:", e.shortMessage || e.message); process.exit(1); });