theseus/lib/resolvers/sns.cjs

84 lines
3.5 KiB
JavaScript
Raw Normal View History

"use strict";
// Solana Name Service resolver — .sol names via Bonfida's public SDK proxy.
// SNS stores URL and IPFS records on-chain; the proxy exposes them as a
// REST endpoint so Theseus doesn't need the Solana web3.js stack bundled in.
//
// Proxy responses (as of 2026-10):
// GET /resolve/<name> → { s: "ok", result: "<address or url>" }
// or { s: "error", result: "..." }
// GET /record/<name>/url → { s: "ok", result: "https://…" }
// GET /record/<name>/ipfs → { s: "ok", result: "<cid>" }
// GET /record/<name>/arwv → { s: "ok", result: "<arweave-tx>" }
//
// We prefer URL (clearnet redirect) over IPFS (gateway) over Arweave
// (gateway) because URL is explicit intent and the user's gateway choice
// shouldn't matter for a plain redirect.
//
// If the user's IP is Cloudflare-blocked for the default proxy, they can
// paste any Solana RPC + sns-sdk-proxy compatible endpoint into Advanced.
const POS_TTL_MS = 10 * 60 * 1000;
const NEG_TTL_MS = 60 * 1000;
const RPC_TIMEOUT_MS = 5000;
const CACHE = new Map();
async function proxyGet(base, path) {
const url = base.replace(/\/+$/, "") + path;
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(new Error("SNS proxy timeout")), RPC_TIMEOUT_MS);
try {
const res = await fetch(url, {
method: "GET",
headers: { "accept": "application/json", "user-agent": "Theseus/0.3.99 (silentmode.st)" },
signal: ctrl.signal,
});
if (!res.ok) throw new Error(`SNS HTTP ${res.status}`);
const json = await res.json();
if (json?.s === "ok" && json.result) return String(json.result);
return null; // no record / name not registered
} finally { clearTimeout(t); }
}
async function resolve(host, { rpc, gateway } = {}) {
if (!host) return null;
const name = String(host).toLowerCase();
if (!name.endsWith(".sol") || name === ".sol" || name === "sol") return null;
const cached = CACHE.get(name);
if (cached && cached.exp > Date.now()) return cached.result;
// "rpc" here is the SNS proxy base URL (not a Solana JSON-RPC); naming
// is for consistency with the other adapters.
const base = rpc || "https://sns-sdk-proxy.bonfida.workers.dev";
const gw = (gateway || "https://ipfs.io").replace(/\/+$/, "");
const bareName = name.replace(/\.sol$/i, "");
try {
// URL takes precedence (explicit redirect intent).
const urlRec = await proxyGet(base, `/record/${encodeURIComponent(bareName)}.sol/url`).catch(() => null);
if (urlRec) {
const u = /^https?:\/\//i.test(urlRec) ? urlRec : `https://${urlRec}`;
const result = { url: u, protocol: "sns", kind: "url" };
CACHE.set(name, { result, exp: Date.now() + POS_TTL_MS });
return result;
}
// IPFS fallback.
const ipfsRec = await proxyGet(base, `/record/${encodeURIComponent(bareName)}.sol/ipfs`).catch(() => null);
if (ipfsRec) {
const result = { url: `${gw}/ipfs/${ipfsRec}`, protocol: "sns", kind: "ipfs", cid: ipfsRec };
CACHE.set(name, { result, exp: Date.now() + POS_TTL_MS });
return result;
}
const result = { error: "no URL or IPFS record on this .sol name" };
CACHE.set(name, { result, exp: Date.now() + NEG_TTL_MS });
return result;
} catch (err) {
const result = { error: String(err?.message || err) };
CACHE.set(name, { result, exp: Date.now() + NEG_TTL_MS });
return result;
}
}
function clearCache() { CACHE.clear(); }
module.exports = { resolve, clearCache };