@bsv/sdk
Version:
BSV Blockchain Software Development Kit
183 lines • 7.44 kB
JavaScript
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