86 lines
2.5 KiB
JavaScript
86 lines
2.5 KiB
JavaScript
|
|
/**
|
||
|
|
* Looking up a BNS name's current on-chain owner via the Silent Mode gateway.
|
||
|
|
*
|
||
|
|
* The listing endpoint uses this to answer "did this publisher really sign
|
||
|
|
* this?" — recover the signer address from the BIP-137 signature (that's just
|
||
|
|
* cryptography) and compare it against what the chain says owns the name
|
||
|
|
* (that's the trust claim).
|
||
|
|
*
|
||
|
|
* Memory [[recovery-is-not-verification]] is what makes this a two-step check
|
||
|
|
* rather than one — recovery always succeeds against *some* key, so we must
|
||
|
|
* compare the result against a claimed destination.
|
||
|
|
*
|
||
|
|
* MOCK_BNS_OWNER=<cashaddr> short-circuits the network call for local tests
|
||
|
|
* where we don't have a chipnet gateway reachable or don't want to depend on
|
||
|
|
* one for a unit test.
|
||
|
|
*/
|
||
|
|
|
||
|
|
const DEFAULT_GATEWAY =
|
||
|
|
(process.env.BNS_NETWORK || 'chipnet').toLowerCase() === 'mainnet'
|
||
|
|
? 'https://silentmode.st'
|
||
|
|
: 'https://navigate.st';
|
||
|
|
|
||
|
|
const BNS_GATEWAY = process.env.BNS_GATEWAY || DEFAULT_GATEWAY;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Fetch a name's record from the gateway. Returns:
|
||
|
|
* {ok: true, owner, records, registered: true}
|
||
|
|
* {ok: false, reason}
|
||
|
|
*
|
||
|
|
* Caches successful lookups for 60 seconds — ownership rarely changes faster
|
||
|
|
* than that, and a listing POST should not thrash the gateway on retries.
|
||
|
|
*/
|
||
|
|
const cache = new Map(); // name -> {at, result}
|
||
|
|
const CACHE_TTL_MS = 60_000;
|
||
|
|
|
||
|
|
export async function lookupName(name) {
|
||
|
|
const mock = process.env.MOCK_BNS_OWNER;
|
||
|
|
if (mock) {
|
||
|
|
return { ok: true, owner: mock, records: {}, registered: true, source: 'mock' };
|
||
|
|
}
|
||
|
|
|
||
|
|
const key = String(name).toLowerCase();
|
||
|
|
const now = Date.now();
|
||
|
|
const cached = cache.get(key);
|
||
|
|
if (cached && now - cached.at < CACHE_TTL_MS) {
|
||
|
|
return cached.result;
|
||
|
|
}
|
||
|
|
|
||
|
|
let resp;
|
||
|
|
try {
|
||
|
|
resp = await fetch(`${BNS_GATEWAY}/api/name/${encodeURIComponent(key)}`, {
|
||
|
|
headers: { accept: 'application/json' },
|
||
|
|
signal: AbortSignal.timeout(5000),
|
||
|
|
});
|
||
|
|
} catch (err) {
|
||
|
|
return { ok: false, reason: `gateway unreachable: ${err?.message || err}` };
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!resp.ok) {
|
||
|
|
return { ok: false, reason: `gateway returned HTTP ${resp.status}` };
|
||
|
|
}
|
||
|
|
|
||
|
|
let body;
|
||
|
|
try {
|
||
|
|
body = await resp.json();
|
||
|
|
} catch (err) {
|
||
|
|
return { ok: false, reason: 'gateway returned non-JSON' };
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!body.registered) {
|
||
|
|
const result = { ok: false, reason: 'name is not registered', body };
|
||
|
|
cache.set(key, { at: now, result });
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
const result = {
|
||
|
|
ok: true,
|
||
|
|
owner: body.owner,
|
||
|
|
records: body.records || {},
|
||
|
|
registered: true,
|
||
|
|
source: 'gateway',
|
||
|
|
};
|
||
|
|
cache.set(key, { at: now, result });
|
||
|
|
return result;
|
||
|
|
}
|