UNPKG

@bsv/sdk

Version:

BSV Blockchain Software Development Kit

183 lines 7.44 kB
import { Transaction } from '../transaction/index.js'; import OverlayAdminTokenTemplate from './OverlayAdminTokenTemplate.js'; /** Default SLAP trackers */ export const DEFAULT_SLAP_TRACKERS = [ // BSVA clusters 'https://overlay-us-1.bsvb.tech', 'https://overlay-eu-1.bsvb.tech', 'https://overlay-ap-1.bsvb.tech', // Babbage primary overlay service 'https://users.bapp.dev' // NOTE: Other entities may submit pull requests to the library if they maintain SLAP overlay services. // Additional trackers run by different entities contribute to greater network resiliency. // It also generally doesn't hurt to have more trackers in this list. // DISCLAIMER: // Trackers known to host invalid or illegal records will be removed at the discretion of the BSV Association. ]; /** Default testnet SLAP trackers */ export const DEFAULT_TESTNET_SLAP_TRACKERS = [ // Babbage primary testnet overlay service 'https://testnet-users.bapp.dev' ]; const MAX_TRACKER_WAIT_TIME = 5000; export class HTTPSOverlayLookupFacilitator { fetchClient; allowHTTP; constructor(httpClient = fetch, allowHTTP = false) { this.fetchClient = httpClient; this.allowHTTP = allowHTTP; } async lookup(url, question, timeout = 5000) { if (!url.startsWith('https:') && !this.allowHTTP) { throw new Error('HTTPS facilitator can only use URLs that start with "https:"'); } const timeoutPromise = new Promise((resolve, reject) => setTimeout(() => reject(new Error('Request timed out')), timeout)); const fetchPromise = fetch(`${url}/lookup`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ service: question.service, query: question.query }) }); const response = (await Promise.race([ fetchPromise, timeoutPromise ])); if (response.ok) { return await response.json(); } else { throw new Error('Failed to facilitate lookup'); } } } /** * Represents an SHIP transaction broadcaster. */ export default class LookupResolver { facilitator; slapTrackers; hostOverrides; additionalHosts; networkPreset; constructor(config = {}) { this.networkPreset = config.networkPreset ?? 'mainnet'; this.facilitator = config.facilitator ?? new HTTPSOverlayLookupFacilitator(undefined, this.networkPreset === 'local'); this.slapTrackers = config.slapTrackers ?? (this.networkPreset === 'mainnet' ? DEFAULT_SLAP_TRACKERS : DEFAULT_TESTNET_SLAP_TRACKERS); this.hostOverrides = config.hostOverrides ?? {}; this.additionalHosts = config.additionalHosts ?? {}; } /** * Given a LookupQuestion, returns a LookupAnswer. Aggregates across multiple services and supports resiliency. */ async query(question, timeout) { let competentHosts = []; if (question.service === 'ls_slap') { competentHosts = this.networkPreset === 'local' ? ['http://localhost:8080'] : this.slapTrackers; } else if (this.hostOverrides[question.service] != null) { competentHosts = this.hostOverrides[question.service]; } else if (this.networkPreset === 'local') { competentHosts = ['http://localhost:8080']; } else { competentHosts = await this.findCompetentHosts(question.service); } if (this.additionalHosts[question.service]?.length > 0) { competentHosts = [ ...competentHosts, ...this.additionalHosts[question.service] ]; } if (competentHosts.length < 1) { throw new Error(`No competent ${this.networkPreset} hosts found by the SLAP trackers for lookup service: ${question.service}`); } // Use Promise.allSettled to handle individual host failures const hostResponses = await Promise.allSettled(competentHosts.map(async (host) => await this.facilitator.lookup(host, question, timeout))); const successfulResponses = hostResponses .filter((result) => result.status === 'fulfilled') .map((result) => result.value); if (successfulResponses.length === 0) { throw new Error('No successful responses from any hosts'); } // Process the successful responses // Aggregate outputs from all successful responses const outputs = new Map(); for (const response of successfulResponses) { if (response.type !== 'output-list') { continue; } try { for (const output of response.outputs) { try { const txId = Transaction.fromBEEF(output.beef).id('hex'); // !! This is STUPIDLY inefficient. const key = `${txId}.${output.outputIndex}`; outputs.set(key, output); } catch { continue; } } } catch (_) { // Error processing output, proceed. } } return { type: 'output-list', outputs: Array.from(outputs.values()) }; } /** * Returns a list of competent hosts for a given lookup service. * @param service Service for which competent hosts are to be returned * @returns Array of hosts competent for resolving queries */ async findCompetentHosts(service) { const query = { service: 'ls_slap', query: { service } }; // Use Promise.allSettled to handle individual SLAP tracker failures const trackerResponses = await Promise.allSettled(this.slapTrackers.map(async (tracker) => await this.facilitator.lookup(tracker, query, MAX_TRACKER_WAIT_TIME))); const hosts = new Set(); for (const result of trackerResponses) { if (result.status === 'fulfilled') { const answer = result.value; if (answer.type !== 'output-list') { // Log invalid response and continue continue; } for (const output of answer.outputs) { try { const tx = Transaction.fromBEEF(output.beef); const script = tx.outputs[output.outputIndex].lockingScript; const parsed = OverlayAdminTokenTemplate.decode(script); if (parsed.topicOrService !== service || parsed.protocol !== 'SLAP') { // Invalid advertisement, skip continue; } hosts.add(parsed.domain); } catch { // Invalid output, skip continue; } } } else { // Log tracker failure and continue continue; } } return [...hosts]; } } //# sourceMappingURL=LookupResolver.js.map