183 lines
6.2 KiB
JavaScript
183 lines
6.2 KiB
JavaScript
|
|
#!/usr/bin/env node
|
||
|
|
/**
|
||
|
|
* Lethean Alias-HNS Resolver
|
||
|
|
*
|
||
|
|
* Bridges the main chain alias system to the HNS sidechain.
|
||
|
|
* For each alias with v=lthn1;hns=name.lthn comment:
|
||
|
|
* 1. Queries HSD for the name's resource records
|
||
|
|
* 2. Verifies the TXT record contains the matching lthn-address
|
||
|
|
* 3. Returns the verified binding (alias ↔ HNS name ↔ DNS records)
|
||
|
|
*
|
||
|
|
* Usage:
|
||
|
|
* node resolver.js [--daemon http://127.0.0.1:46941] [--hsd http://127.0.0.1:14037]
|
||
|
|
*/
|
||
|
|
|
||
|
|
const DAEMON_URL = process.env.DAEMON_URL || 'http://127.0.0.1:46941';
|
||
|
|
const HSD_URL = process.env.HSD_URL || 'http://127.0.0.1:14037';
|
||
|
|
const HSD_API_KEY = process.env.HSD_API_KEY || 'testkey';
|
||
|
|
|
||
|
|
async function rpcDaemon(method, params = {}) {
|
||
|
|
const resp = await fetch(`${DAEMON_URL}/json_rpc`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({ jsonrpc: '2.0', id: '0', method, params })
|
||
|
|
});
|
||
|
|
const data = await resp.json();
|
||
|
|
if (data.error) throw new Error(`Daemon ${method}: ${data.error.message}`);
|
||
|
|
return data.result;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function rpcHSD(method, params = []) {
|
||
|
|
const auth = Buffer.from(`x:${HSD_API_KEY}`).toString('base64');
|
||
|
|
const resp = await fetch(HSD_URL, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'Authorization': `Basic ${auth}`,
|
||
|
|
},
|
||
|
|
body: JSON.stringify({ method, params })
|
||
|
|
});
|
||
|
|
const data = await resp.json();
|
||
|
|
if (data.error) throw new Error(`HSD ${method}: ${data.error.message}`);
|
||
|
|
return data.result;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function hsdGetNameResource(name) {
|
||
|
|
try {
|
||
|
|
return await rpcHSD('getnameresource', [name]);
|
||
|
|
} catch {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseComment(comment) {
|
||
|
|
if (!comment || !comment.includes('v=lthn1')) return null;
|
||
|
|
const fields = {};
|
||
|
|
comment.split(';').forEach(pair => {
|
||
|
|
const [key, ...rest] = pair.trim().split('=');
|
||
|
|
if (key && rest.length) fields[key.trim()] = rest.join('=').trim();
|
||
|
|
});
|
||
|
|
if (fields.v !== 'lthn1') return null;
|
||
|
|
return {
|
||
|
|
hns: fields.hns || null, // explicit override, usually derived from alias name
|
||
|
|
type: fields.type || 'user',
|
||
|
|
capabilities: fields.cap ? fields.cap.split(',') : [],
|
||
|
|
tls: fields.tls || null,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Resolve and verify a single alias → HNS binding
|
||
|
|
*/
|
||
|
|
async function resolveAlias(alias) {
|
||
|
|
// 1. Get alias details from main chain
|
||
|
|
const result = await rpcDaemon('get_alias_details', { alias: alias.alias || alias });
|
||
|
|
if (result.status !== 'OK') return { alias: alias.alias || alias, verified: false, reason: 'alias not found' };
|
||
|
|
|
||
|
|
const details = result.alias_details;
|
||
|
|
const parsed = parseComment(details.comment);
|
||
|
|
if (!parsed) return { alias: alias.alias || alias, verified: false, reason: 'no v=lthn1 comment' };
|
||
|
|
|
||
|
|
// 2. HNS name: explicit from hns= field, or derived from alias name
|
||
|
|
// @charon → charon.lthn (implicit), hns=custom.lthn (explicit override)
|
||
|
|
const aliasName = alias.alias || alias;
|
||
|
|
const hnsFullName = parsed.hns || `${aliasName}.lthn`;
|
||
|
|
const hnsName = hnsFullName.replace(/\.lthn$/, '');
|
||
|
|
|
||
|
|
// 3. Query HSD for the name's resource records
|
||
|
|
const resource = await hsdGetNameResource(hnsName);
|
||
|
|
if (!resource || !resource.records) return { alias: alias.alias || alias, hns: parsed.hns, verified: false, reason: 'HNS name not found or no records' };
|
||
|
|
|
||
|
|
// 4. Find TXT record with lthn-address
|
||
|
|
let hnsAddress = null;
|
||
|
|
let dnsRecords = [];
|
||
|
|
for (const record of resource.records) {
|
||
|
|
dnsRecords.push(record);
|
||
|
|
if (record.type === 'TXT' && record.txt) {
|
||
|
|
for (const txt of record.txt) {
|
||
|
|
const match = txt.match(/lthn-address=([a-zA-Z0-9]+)/);
|
||
|
|
if (match) hnsAddress = match[1];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 5. Verify the chain address matches the HNS TXT record
|
||
|
|
const verified = hnsAddress === details.address;
|
||
|
|
|
||
|
|
return {
|
||
|
|
alias: alias.alias || alias,
|
||
|
|
address: details.address,
|
||
|
|
hns: parsed.hns,
|
||
|
|
hnsName,
|
||
|
|
type: parsed.type,
|
||
|
|
capabilities: parsed.capabilities,
|
||
|
|
tls: parsed.tls,
|
||
|
|
dnsRecords,
|
||
|
|
hnsAddress,
|
||
|
|
verified,
|
||
|
|
reason: verified ? 'chain address matches HNS TXT record' : `mismatch: chain=${details.address?.slice(0,16)} hns=${hnsAddress?.slice(0,16) || 'none'}`,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Resolve all aliases and verify HNS bindings
|
||
|
|
*/
|
||
|
|
async function resolveAll() {
|
||
|
|
const chainInfo = await rpcDaemon('getinfo');
|
||
|
|
console.log(`Chain: height=${chainInfo.height}, aliases=${chainInfo.alias_count}`);
|
||
|
|
|
||
|
|
let hsdInfo;
|
||
|
|
try {
|
||
|
|
hsdInfo = await rpcHSD('getinfo');
|
||
|
|
console.log(`HSD: height=${hsdInfo.blocks}, network=${hsdInfo.network}`);
|
||
|
|
} catch (e) {
|
||
|
|
console.log(`HSD: not reachable (${e.message})`);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (chainInfo.alias_count === 0) {
|
||
|
|
console.log('\nNo aliases registered yet (requires HF4)');
|
||
|
|
console.log('When aliases exist, this resolver will:');
|
||
|
|
console.log(' 1. Read all aliases from chain');
|
||
|
|
console.log(' 2. Parse v=lthn1 comments for HNS bindings');
|
||
|
|
console.log(' 3. Query HSD for each name\'s DNS records');
|
||
|
|
console.log(' 4. Verify chain address matches HNS TXT lthn-address');
|
||
|
|
console.log(' 5. Output verified bindings as JSON');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const allAliases = await rpcDaemon('get_all_alias_details', {});
|
||
|
|
const aliases = allAliases.aliases || [];
|
||
|
|
|
||
|
|
const results = [];
|
||
|
|
for (const alias of aliases) {
|
||
|
|
const parsed = parseComment(alias.comment);
|
||
|
|
if (parsed) { // v=lthn1 present — HNS name derived from alias if not explicit
|
||
|
|
const resolved = await resolveAlias(alias);
|
||
|
|
results.push(resolved);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log(`\nResolved ${results.length} aliases with HNS bindings:`);
|
||
|
|
for (const r of results) {
|
||
|
|
const status = r.verified ? '✓ VERIFIED' : '✗ UNVERIFIED';
|
||
|
|
console.log(` ${r.alias}.lthn → ${r.hns} [${status}] ${r.reason}`);
|
||
|
|
if (r.dnsRecords) {
|
||
|
|
for (const rec of r.dnsRecords) {
|
||
|
|
console.log(` ${rec.type}: ${JSON.stringify(rec).slice(0, 80)}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Write registry
|
||
|
|
const fs = await import('fs');
|
||
|
|
fs.writeFileSync('/tmp/lthn-resolved-registry.json', JSON.stringify({
|
||
|
|
timestamp: new Date().toISOString(),
|
||
|
|
chain: { height: chainInfo.height, aliases: chainInfo.alias_count },
|
||
|
|
hsd: hsdInfo ? { height: hsdInfo.blocks, network: hsdInfo.network } : null,
|
||
|
|
bindings: results,
|
||
|
|
}, null, 2));
|
||
|
|
console.log('\nRegistry written to /tmp/lthn-resolved-registry.json');
|
||
|
|
}
|
||
|
|
|
||
|
|
resolveAll().catch(console.error);
|