#!/usr/bin/env node
/*
* Register a Farcaster identity from this machine.
* node farcaster.js check - read-only: balances, price, whether we already have an fid
* node farcaster.js register - step 1: buy the fid (spends ETH)
* node farcaster.js signer - step 2: add an ed25519 signer key so we can actually cast
*
* Every write step is idempotent: it checks on-chain state first and bails if already done.
*/
const { JsonRpcProvider, Wallet, Contract, formatEther, parseEther, hexlify } = require("ethers");
const crypto = require("crypto");
const fs = require("fs");
const RPCS = ["https://mainnet.optimism.io", "https://optimism.publicnode.com", "https://op-pokt.nodies.app"];
const OUT = __dirname + "/farcaster.json";
const ID_GATEWAY = "0x00000000Fc25870C6eD6b6c7E41Fb078b7656f69";
const ID_REGISTRY = "0x00000000Fc6c5F01Fc30151999387Bb99A9f489b";
const KEY_GATEWAY = "0x00000000fC56947c7E7183f8Ca4B62398CaAdf0B";
const SKR_VALIDATOR= "0x00000000FC700472606ED4fA22623Acf62c60553";
const idGatewayAbi = [
"function price() view returns (uint256)",
"function register(address recovery) payable returns (uint256 fid, uint256 overpayment)",
];
const idRegistryAbi = ["function idOf(address) view returns (uint256)"];
const keyGatewayAbi = ["function add(uint32 keyType, bytes key, uint8 metadataType, bytes metadata)"];
const skrAbi = [
"function encodeMetadata((uint256 requestFid, address requestSigner, bytes signature, uint256 deadline)) view returns (bytes)",
];
async function provider() {
let last;
for (const url of RPCS) {
try { const p = new JsonRpcProvider(url); await p.getBlockNumber(); return p; }
catch (e) { last = e; }
}
throw last;
}
function loadState() {
try { return JSON.parse(fs.readFileSync(OUT, "utf8")); } catch { return {}; }
}
function saveState(s) {
fs.writeFileSync(OUT, JSON.stringify(s, null, 2));
fs.chmodSync(OUT, 0o600);
}
async function main() {
const cmd = process.argv[2] || "check";
const keys = JSON.parse(fs.readFileSync(__dirname + "/keys.json", "utf8"));
const p = await provider();
const w = new Wallet(keys.privateKey, p);
const idGateway = new Contract(ID_GATEWAY, idGatewayAbi, w);
const idRegistry = new Contract(ID_REGISTRY, idRegistryAbi, p);
const bal = await p.getBalance(w.address);
const price = await idGateway["price()"]();
const existing = await idRegistry.idOf(w.address);
const state = loadState();
console.log("address :", w.address);
console.log("balance :", formatEther(bal), "ETH on Optimism");
console.log("fid cost:", formatEther(price), "ETH");
console.log("fid :", existing.toString() === "0" ? "(none yet)" : existing.toString());
if (cmd === "check") {
const need = price + parseEther("0.00005"); // + headroom for gas
console.log(bal >= need ? "\nREADY: enough ETH to register." :
`\nWAITING: need ~${formatEther(need)} ETH, short by ${formatEther(need - bal)}.`);
return;
}
if (cmd === "register") {
if (existing.toString() !== "0") { console.log("\nAlready registered. Nothing to do."); return; }
if (bal < price) throw new Error("not enough ETH to cover the registration price");
// recovery address = self. No third party can claw the account back.
const tx = await idGateway.register(w.address, { value: price });
console.log("\nsent:", tx.hash, "— waiting for confirmation…");
const rc = await tx.wait();
const fid = await idRegistry.idOf(w.address);
console.log("CONFIRMED in block", rc.blockNumber, "→ FID", fid.toString());
saveState({ ...state, fid: fid.toString(), custody: w.address, registerTx: tx.hash });
return;
}
if (cmd === "signer") {
const fid = existing;
if (fid.toString() === "0") throw new Error("register an fid first");
if (state.signerPublicKey) { console.log("\nSigner already added:", state.signerPublicKey); return; }
// ed25519 keypair — this is what actually signs casts
const kp = crypto.generateKeyPairSync("ed25519");
const der = kp.publicKey.export({ type: "spki", format: "der" });
const pub = hexlify(der.subarray(der.length - 32)); // raw 32-byte pubkey
const priv = kp.privateKey.export({ type: "pkcs8", format: "der" });
const deadline = Math.floor(Date.now() / 1000) + 3600;
// Self-signed: we are our own "app", which is allowed since we control the custody key.
const sig = await w.signTypedData(
{ name: "Farcaster SignedKeyRequestValidator", version: "1",
chainId: 10, verifyingContract: SKR_VALIDATOR },
{ SignedKeyRequest: [
{ name: "requestFid", type: "uint256" },
{ name: "key", type: "bytes" },
{ name: "deadline", type: "uint256" } ] },
{ requestFid: fid, key: pub, deadline }
);
// encodeMetadata must come from the validator contract; hand-rolled ABI encoding
// gets the dynamic offset pointer wrong.
const skr = new Contract(SKR_VALIDATOR, skrAbi, p);
const metadata = await skr.encodeMetadata({
requestFid: fid, requestSigner: w.address, signature: sig, deadline });
const keyGateway = new Contract(KEY_GATEWAY, keyGatewayAbi, w);
const tx = await keyGateway.add(1, pub, 1, metadata);
console.log("\nsent:", tx.hash, "— waiting…");
const rc = await tx.wait();
console.log("CONFIRMED in block", rc.blockNumber);
saveState({ ...state, fid: fid.toString(), custody: w.address,
signerPublicKey: pub, signerPrivateKeyPkcs8: priv.toString("base64"),
signerTx: tx.hash });
console.log("signer public key:", pub);
return;
}
throw new Error("unknown command: " + cmd);
}
main().catch(e => { console.error("ERROR:", e.shortMessage || e.message); process.exit(1); });