#!/usr/bin/env node
/*
* Post a cast to Farcaster using the ed25519 signer registered by farcaster.js.
* node cast.js "hello world"
* node cast.js --file note.txt
* node cast.js --reply <parentFid> <parentHashHex> "text"
* Prints the resulting cast hash. Dry run with --dry.
*/
const fs = require("fs");
const {
makeCastAdd, NobleEd25519Signer, FarcasterNetwork, Message,
} = require("@farcaster/core");
const STATE = __dirname + "/farcaster.json";
// hubs that accept writes; tried in order
const HUBS = [
"https://snap.farcaster.xyz:3381",
"https://hub.pinata.cloud",
];
function signerFromState(st) {
// we stored the ed25519 key as PKCS8 DER (base64). The raw 32-byte seed is the tail.
const der = Buffer.from(st.signerPrivateKeyPkcs8, "base64");
return new NobleEd25519Signer(der.subarray(der.length - 32));
}
async function submit(bytes) {
let lastErr;
for (const hub of HUBS) {
try {
const r = await fetch(hub + "/v1/submitMessage", {
method: "POST",
headers: { "Content-Type": "application/octet-stream" },
body: bytes,
});
const body = await r.text();
if (r.ok) return { hub, body };
lastErr = `${hub} -> ${r.status} ${body.slice(0, 300)}`;
} catch (e) {
lastErr = `${hub} -> ${e.message}`;
}
}
throw new Error("all hubs rejected the message:\n" + lastErr);
}
(async () => {
const args = process.argv.slice(2);
const dry = args.includes("--dry");
const rest = args.filter(a => a !== "--dry");
let text, parent = null;
if (rest[0] === "--file") {
text = fs.readFileSync(rest[1], "utf8").trim();
} else if (rest[0] === "--reply") {
parent = { fid: Number(rest[1]), hash: Buffer.from(rest[2].replace(/^0x/, ""), "hex") };
text = rest.slice(3).join(" ");
} else {
text = rest.join(" ");
}
if (!text) throw new Error("nothing to say");
// Farcaster counts bytes, not characters, and the limit is 320 for a standard cast.
const bytes = Buffer.byteLength(text, "utf8");
if (bytes > 320) throw new Error(`cast is ${bytes} bytes; limit is 320. Trim it.`);
const st = JSON.parse(fs.readFileSync(STATE, "utf8"));
const fid = Number(st.fid);
const signer = signerFromState(st);
const body = { text, embeds: [], embedsDeprecated: [], mentions: [], mentionsPositions: [] };
if (parent) body.parentCastId = parent;
const res = await makeCastAdd(body, { fid, network: FarcasterNetwork.MAINNET }, signer);
if (res.isErr()) throw res.error;
const msg = res.value;
const encoded = Message.encode(msg).finish();
const hash = "0x" + Buffer.from(msg.hash).toString("hex");
console.log(`fid ${fid} · ${bytes} bytes · hash ${hash}`);
if (dry) { console.log("(dry run — not submitted)"); return; }
const out = await submit(encoded);
console.log("accepted by", out.hub);
console.log("view: https://farcaster.xyz/~/conversations/" + hash);
})().catch(e => { console.error("ERROR:", e.message); process.exit(1); });