2020-12-28 04:47:22 +07:00
|
|
|
import config from '../../config';
|
2023-11-21 17:20:11 +09:00
|
|
|
import axios, { AxiosResponse, isAxiosError } from 'axios';
|
2023-02-07 20:56:33 -06:00
|
|
|
import http from 'http';
|
2024-03-03 20:29:54 +00:00
|
|
|
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
|
2020-12-28 04:47:22 +07:00
|
|
|
import { IEsploraApi } from './esplora-api.interface';
|
2023-05-03 10:11:44 +04:00
|
|
|
import logger from '../../logger';
|
2024-02-06 23:42:14 +00:00
|
|
|
import { Common } from '../common';
|
2024-03-23 11:27:28 +00:00
|
|
|
import { TestMempoolAcceptResult } from './bitcoin-api.interface';
|
2020-12-28 04:47:22 +07:00
|
|
|
|
2023-08-05 13:08:47 +09:00
|
|
|
interface FailoverHost {
|
|
|
|
host: string,
|
2023-08-05 19:55:33 +09:00
|
|
|
rtts: number[],
|
2023-11-14 09:55:02 +00:00
|
|
|
rtt: number,
|
2023-11-21 17:20:11 +09:00
|
|
|
timedOut?: boolean,
|
2023-08-05 13:08:47 +09:00
|
|
|
failures: number,
|
2023-11-14 09:55:02 +00:00
|
|
|
latestHeight?: number,
|
2023-08-05 13:08:47 +09:00
|
|
|
socket?: boolean,
|
|
|
|
outOfSync?: boolean,
|
|
|
|
unreachable?: boolean,
|
|
|
|
preferred?: boolean,
|
2024-02-06 23:42:14 +00:00
|
|
|
checked: boolean,
|
2024-03-06 19:34:12 +00:00
|
|
|
lastChecked?: number,
|
2023-08-05 13:08:47 +09:00
|
|
|
}
|
2023-02-07 20:56:33 -06:00
|
|
|
|
2023-08-05 13:08:47 +09:00
|
|
|
class FailoverRouter {
|
|
|
|
activeHost: FailoverHost;
|
|
|
|
fallbackHost: FailoverHost;
|
2024-02-06 23:42:14 +00:00
|
|
|
maxHeight: number = 0;
|
2023-08-05 13:08:47 +09:00
|
|
|
hosts: FailoverHost[];
|
|
|
|
multihost: boolean;
|
|
|
|
pollInterval: number = 60000;
|
|
|
|
pollTimer: NodeJS.Timeout | null = null;
|
|
|
|
pollConnection = axios.create();
|
|
|
|
requestConnection = axios.create({
|
|
|
|
httpAgent: new http.Agent({ keepAlive: true })
|
|
|
|
});
|
2023-05-03 10:11:44 +04:00
|
|
|
|
|
|
|
constructor() {
|
2023-08-05 13:08:47 +09:00
|
|
|
// setup list of hosts
|
|
|
|
this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => {
|
|
|
|
return {
|
2023-08-05 20:06:19 +09:00
|
|
|
host: domain,
|
2024-02-06 23:42:14 +00:00
|
|
|
checked: false,
|
2023-08-05 19:55:33 +09:00
|
|
|
rtts: [],
|
|
|
|
rtt: Infinity,
|
2023-08-05 13:08:47 +09:00
|
|
|
failures: 0,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
this.activeHost = {
|
|
|
|
host: config.ESPLORA.UNIX_SOCKET_PATH || config.ESPLORA.REST_API_URL,
|
2023-08-05 19:55:33 +09:00
|
|
|
rtts: [],
|
|
|
|
rtt: 0,
|
2023-08-05 13:08:47 +09:00
|
|
|
failures: 0,
|
|
|
|
socket: !!config.ESPLORA.UNIX_SOCKET_PATH,
|
|
|
|
preferred: true,
|
2024-02-06 23:42:14 +00:00
|
|
|
checked: false,
|
2023-08-05 13:08:47 +09:00
|
|
|
};
|
|
|
|
this.fallbackHost = this.activeHost;
|
|
|
|
this.hosts.unshift(this.activeHost);
|
|
|
|
this.multihost = this.hosts.length > 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
public startHealthChecks(): void {
|
2023-08-05 19:55:33 +09:00
|
|
|
// use axios interceptors to measure request rtt
|
2023-08-05 13:08:47 +09:00
|
|
|
this.pollConnection.interceptors.request.use((config) => {
|
|
|
|
config['meta'] = { startTime: Date.now() };
|
|
|
|
return config;
|
|
|
|
});
|
|
|
|
this.pollConnection.interceptors.response.use((response) => {
|
2023-08-05 19:55:33 +09:00
|
|
|
response.config['meta'].rtt = Date.now() - response.config['meta'].startTime;
|
2023-08-05 13:08:47 +09:00
|
|
|
return response;
|
|
|
|
});
|
2023-04-05 16:27:13 +09:00
|
|
|
|
2023-08-05 13:08:47 +09:00
|
|
|
if (this.multihost) {
|
|
|
|
this.pollHosts();
|
|
|
|
}
|
2023-05-03 10:11:44 +04:00
|
|
|
}
|
|
|
|
|
2023-08-05 19:55:33 +09:00
|
|
|
// start polling hosts to measure availability & rtt
|
2023-08-05 13:08:47 +09:00
|
|
|
private async pollHosts(): Promise<void> {
|
|
|
|
if (this.pollTimer) {
|
|
|
|
clearTimeout(this.pollTimer);
|
|
|
|
}
|
|
|
|
|
2024-02-06 23:42:14 +00:00
|
|
|
const start = Date.now();
|
2023-08-05 13:08:47 +09:00
|
|
|
|
2023-08-05 19:55:33 +09:00
|
|
|
// update rtts & sync status
|
2024-02-06 23:42:14 +00:00
|
|
|
for (const host of this.hosts) {
|
|
|
|
try {
|
|
|
|
const result = await (host.socket
|
|
|
|
? this.pollConnection.get<number>('/blocks/tip/height', { socketPath: host.host, timeout: config.ESPLORA.FALLBACK_TIMEOUT })
|
|
|
|
: this.pollConnection.get<number>(host.host + '/blocks/tip/height', { timeout: config.ESPLORA.FALLBACK_TIMEOUT })
|
|
|
|
);
|
|
|
|
if (result) {
|
|
|
|
const height = result.data;
|
|
|
|
this.maxHeight = Math.max(height, this.maxHeight);
|
|
|
|
const rtt = result.config['meta'].rtt;
|
|
|
|
host.rtts.unshift(rtt);
|
|
|
|
host.rtts.slice(0, 5);
|
|
|
|
host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length;
|
|
|
|
host.latestHeight = height;
|
|
|
|
if (height == null || isNaN(height) || (this.maxHeight - height > 2)) {
|
|
|
|
host.outOfSync = true;
|
|
|
|
} else {
|
|
|
|
host.outOfSync = false;
|
|
|
|
}
|
|
|
|
host.unreachable = false;
|
2023-05-03 10:11:44 +04:00
|
|
|
} else {
|
2024-02-06 23:42:14 +00:00
|
|
|
host.outOfSync = true;
|
|
|
|
host.unreachable = true;
|
|
|
|
host.rtts = [];
|
|
|
|
host.rtt = Infinity;
|
2023-05-03 10:11:44 +04:00
|
|
|
}
|
2023-11-21 17:20:11 +09:00
|
|
|
host.timedOut = false;
|
2024-02-06 23:42:14 +00:00
|
|
|
} catch (e) {
|
2023-11-14 10:17:02 +00:00
|
|
|
host.outOfSync = true;
|
2023-08-05 13:08:47 +09:00
|
|
|
host.unreachable = true;
|
2024-02-06 23:42:14 +00:00
|
|
|
host.rtts = [];
|
|
|
|
host.rtt = Infinity;
|
2023-11-21 17:20:11 +09:00
|
|
|
if (isAxiosError(e) && (e.code === 'ECONNABORTED' || e.code === 'ETIMEDOUT')) {
|
|
|
|
host.timedOut = true;
|
|
|
|
} else {
|
|
|
|
host.timedOut = false;
|
|
|
|
}
|
2024-02-06 23:42:14 +00:00
|
|
|
}
|
|
|
|
host.checked = true;
|
2024-03-06 19:34:12 +00:00
|
|
|
host.lastChecked = Date.now();
|
2024-02-06 23:42:14 +00:00
|
|
|
|
|
|
|
// switch if the current host is out of sync or significantly slower than the next best alternative
|
|
|
|
const rankOrder = this.sortHosts();
|
|
|
|
// switch if the current host is out of sync or significantly slower than the next best alternative
|
|
|
|
if (this.activeHost.outOfSync || this.activeHost.unreachable || (this.activeHost !== rankOrder[0] && rankOrder[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (rankOrder[0].rtt * 2) + 50)) {
|
|
|
|
if (this.activeHost.unreachable) {
|
|
|
|
logger.warn(`🚨🚨🚨 Unable to reach ${this.activeHost.host}, failing over to next best alternative 🚨🚨🚨`);
|
|
|
|
} else if (this.activeHost.outOfSync) {
|
|
|
|
logger.warn(`🚨🚨🚨 ${this.activeHost.host} has fallen behind, failing over to next best alternative 🚨🚨🚨`);
|
|
|
|
} else {
|
|
|
|
logger.debug(`🛠️ ${this.activeHost.host} is no longer the best esplora host 🛠️`);
|
|
|
|
}
|
|
|
|
this.electHost();
|
2023-08-05 13:08:47 +09:00
|
|
|
}
|
2024-02-06 23:42:14 +00:00
|
|
|
await Common.sleep$(50);
|
2023-08-05 13:08:47 +09:00
|
|
|
}
|
|
|
|
|
2024-02-06 23:42:14 +00:00
|
|
|
const rankOrder = this.updateFallback();
|
|
|
|
logger.debug(`Tomahawk ranking:\n${rankOrder.map((host, index) => this.formatRanking(index, host, this.activeHost, this.maxHeight)).join('\n')}`);
|
2023-08-05 13:08:47 +09:00
|
|
|
|
2024-02-06 23:42:14 +00:00
|
|
|
const elapsed = Date.now() - start;
|
2023-08-05 19:17:20 +09:00
|
|
|
|
2024-02-06 23:42:14 +00:00
|
|
|
this.pollTimer = setTimeout(() => { this.pollHosts(); }, Math.max(1, this.pollInterval - elapsed));
|
2023-08-05 13:08:47 +09:00
|
|
|
}
|
|
|
|
|
2023-11-14 09:55:02 +00:00
|
|
|
private formatRanking(index: number, host: FailoverHost, active: FailoverHost, maxHeight: number): string {
|
2024-02-06 23:42:14 +00:00
|
|
|
const heightStatus = !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '✅'));
|
2023-11-21 17:20:11 +09:00
|
|
|
return `${host === active ? '⭐️' : ' '} ${host.rtt < Infinity ? Math.round(host.rtt).toString().padStart(5, ' ') + 'ms' : (host.timedOut ? ' ⌛️💥 ' : ' - ')} ${!host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅')} | block: ${host.latestHeight || '??????'} ${heightStatus} | ${host.host} ${host === active ? '⭐️' : ' '}`;
|
2024-02-06 23:42:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private updateFallback(): FailoverHost[] {
|
|
|
|
const rankOrder = this.sortHosts();
|
|
|
|
if (rankOrder.length > 1 && rankOrder[0] === this.activeHost) {
|
|
|
|
this.fallbackHost = rankOrder[1];
|
|
|
|
} else {
|
|
|
|
this.fallbackHost = rankOrder[0];
|
|
|
|
}
|
|
|
|
return rankOrder;
|
2023-11-14 09:55:02 +00:00
|
|
|
}
|
|
|
|
|
2023-08-05 13:08:47 +09:00
|
|
|
// sort hosts by connection quality, and update default fallback
|
2024-03-03 20:29:54 +00:00
|
|
|
public sortHosts(): FailoverHost[] {
|
2023-08-05 13:08:47 +09:00
|
|
|
// sort by connection quality
|
2024-02-06 23:42:14 +00:00
|
|
|
return this.hosts.slice().sort((a, b) => {
|
2023-08-05 13:08:47 +09:00
|
|
|
if ((a.unreachable || a.outOfSync) === (b.unreachable || b.outOfSync)) {
|
|
|
|
if (a.preferred === b.preferred) {
|
2023-08-05 19:55:33 +09:00
|
|
|
// lower rtt is best
|
|
|
|
return a.rtt - b.rtt;
|
2023-08-05 13:08:47 +09:00
|
|
|
} else { // unless we have a preferred host
|
|
|
|
return a.preferred ? -1 : 1;
|
|
|
|
}
|
|
|
|
} else { // or the host is out of sync
|
|
|
|
return (a.unreachable || a.outOfSync) ? 1 : -1;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// depose the active host and choose the next best replacement
|
|
|
|
private electHost(): void {
|
|
|
|
this.activeHost.outOfSync = true;
|
|
|
|
this.activeHost.failures = 0;
|
2024-02-06 23:42:14 +00:00
|
|
|
const rankOrder = this.sortHosts();
|
|
|
|
this.activeHost = rankOrder[0];
|
2023-08-05 13:08:47 +09:00
|
|
|
logger.warn(`Switching esplora host to ${this.activeHost.host}`);
|
2023-05-03 10:11:44 +04:00
|
|
|
}
|
2020-12-28 04:47:22 +07:00
|
|
|
|
2023-08-05 13:08:47 +09:00
|
|
|
private addFailure(host: FailoverHost): FailoverHost {
|
|
|
|
host.failures++;
|
|
|
|
if (host.failures > 5 && this.multihost) {
|
2023-11-14 09:55:02 +00:00
|
|
|
logger.warn(`🚨🚨🚨 Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative 🚨🚨🚨`);
|
2023-08-05 13:08:47 +09:00
|
|
|
this.electHost();
|
|
|
|
return this.activeHost;
|
|
|
|
} else {
|
|
|
|
return this.fallbackHost;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private async $query<T>(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true): Promise<T> {
|
|
|
|
let axiosConfig;
|
|
|
|
let url;
|
|
|
|
if (host.socket) {
|
2023-09-14 22:57:37 +00:00
|
|
|
axiosConfig = { socketPath: host.host, timeout: config.ESPLORA.REQUEST_TIMEOUT, responseType };
|
2023-08-05 13:08:47 +09:00
|
|
|
url = path;
|
|
|
|
} else {
|
2023-09-14 22:57:37 +00:00
|
|
|
axiosConfig = { timeout: config.ESPLORA.REQUEST_TIMEOUT, responseType };
|
2023-08-05 13:08:47 +09:00
|
|
|
url = host.host + path;
|
|
|
|
}
|
2023-08-16 02:25:48 +09:00
|
|
|
if (data?.params) {
|
|
|
|
axiosConfig.params = data.params;
|
|
|
|
}
|
2023-08-05 13:08:47 +09:00
|
|
|
return (method === 'post'
|
|
|
|
? this.requestConnection.post<T>(url, data, axiosConfig)
|
|
|
|
: this.requestConnection.get<T>(url, axiosConfig)
|
|
|
|
).then((response) => { host.failures = Math.max(0, host.failures - 1); return response.data; })
|
2023-08-02 13:24:56 +09:00
|
|
|
.catch((e) => {
|
2023-08-05 13:08:47 +09:00
|
|
|
let fallbackHost = this.fallbackHost;
|
|
|
|
if (e?.response?.status !== 404) {
|
2023-11-12 09:23:37 +00:00
|
|
|
logger.warn(`esplora request failed ${e?.response?.status} ${host.host}${path}`);
|
|
|
|
logger.warn(e instanceof Error ? e.message : e);
|
2023-08-05 13:08:47 +09:00
|
|
|
fallbackHost = this.addFailure(host);
|
|
|
|
}
|
|
|
|
if (retry && e?.code === 'ECONNREFUSED' && this.multihost) {
|
2023-08-02 13:24:56 +09:00
|
|
|
// Retry immediately
|
2023-08-05 13:08:47 +09:00
|
|
|
return this.$query(method, path, data, responseType, fallbackHost, false);
|
2023-08-02 13:24:56 +09:00
|
|
|
} else {
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-08-16 02:25:48 +09:00
|
|
|
public async $get<T>(path, responseType = 'json', params: any = null): Promise<T> {
|
|
|
|
return this.$query<T>('get', path, params ? { params } : null, responseType);
|
2023-08-05 13:08:47 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
public async $post<T>(path, data: any, responseType = 'json'): Promise<T> {
|
2023-08-05 19:17:20 +09:00
|
|
|
return this.$query<T>('post', path, data, responseType);
|
2023-08-05 13:08:47 +09:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class ElectrsApi implements AbstractBitcoinApi {
|
|
|
|
private failoverRouter = new FailoverRouter();
|
|
|
|
|
2020-12-28 04:47:22 +07:00
|
|
|
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
|
2023-08-05 13:08:47 +09:00
|
|
|
return this.failoverRouter.$get<IEsploraApi.Transaction['txid'][]>('/mempool/txids');
|
2020-12-28 04:47:22 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
$getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> {
|
2023-08-05 13:08:47 +09:00
|
|
|
return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txId);
|
2020-12-28 04:47:22 +07:00
|
|
|
}
|
|
|
|
|
2023-08-05 16:08:54 +09:00
|
|
|
async $getRawTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> {
|
2023-08-28 16:55:50 +09:00
|
|
|
return this.failoverRouter.$post<IEsploraApi.Transaction[]>('/internal/txs', txids, 'json');
|
2023-08-05 16:08:54 +09:00
|
|
|
}
|
|
|
|
|
2023-08-02 13:24:56 +09:00
|
|
|
async $getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> {
|
2023-08-28 02:18:59 +09:00
|
|
|
return this.failoverRouter.$post<IEsploraApi.Transaction[]>('/internal/mempool/txs', txids, 'json');
|
2023-08-02 13:24:56 +09:00
|
|
|
}
|
|
|
|
|
2023-11-15 06:57:31 +00:00
|
|
|
async $getAllMempoolTransactions(lastSeenTxid?: string, max_txs?: number): Promise<IEsploraApi.Transaction[]> {
|
|
|
|
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/internal/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''), 'json', max_txs ? { max_txs } : null);
|
2023-07-22 14:09:11 +09:00
|
|
|
}
|
|
|
|
|
2022-11-22 21:45:05 +09:00
|
|
|
$getTransactionHex(txId: string): Promise<string> {
|
2023-08-05 13:08:47 +09:00
|
|
|
return this.failoverRouter.$get<string>('/tx/' + txId + '/hex');
|
2022-11-22 21:45:05 +09:00
|
|
|
}
|
|
|
|
|
2020-12-28 04:47:22 +07:00
|
|
|
$getBlockHeightTip(): Promise<number> {
|
2023-08-05 13:08:47 +09:00
|
|
|
return this.failoverRouter.$get<number>('/blocks/tip/height');
|
2020-12-28 04:47:22 +07:00
|
|
|
}
|
|
|
|
|
2022-06-22 13:15:44 +02:00
|
|
|
$getBlockHashTip(): Promise<string> {
|
2023-08-05 13:08:47 +09:00
|
|
|
return this.failoverRouter.$get<string>('/blocks/tip/hash');
|
2022-06-22 13:15:44 +02:00
|
|
|
}
|
|
|
|
|
2020-12-28 04:47:22 +07:00
|
|
|
$getTxIdsForBlock(hash: string): Promise<string[]> {
|
2023-08-05 13:08:47 +09:00
|
|
|
return this.failoverRouter.$get<string[]>('/block/' + hash + '/txids');
|
2020-12-28 04:47:22 +07:00
|
|
|
}
|
|
|
|
|
2023-07-24 16:58:30 +09:00
|
|
|
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
|
2023-09-06 08:24:30 +09:00
|
|
|
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/internal/block/' + hash + '/txs');
|
2023-07-24 16:58:30 +09:00
|
|
|
}
|
|
|
|
|
2020-12-28 04:47:22 +07:00
|
|
|
$getBlockHash(height: number): Promise<string> {
|
2023-08-05 13:08:47 +09:00
|
|
|
return this.failoverRouter.$get<string>('/block-height/' + height);
|
2020-12-28 04:47:22 +07:00
|
|
|
}
|
|
|
|
|
2021-07-19 04:56:16 +05:30
|
|
|
$getBlockHeader(hash: string): Promise<string> {
|
2023-08-05 13:08:47 +09:00
|
|
|
return this.failoverRouter.$get<string>('/block/' + hash + '/header');
|
2021-07-19 04:56:16 +05:30
|
|
|
}
|
|
|
|
|
2020-12-28 04:47:22 +07:00
|
|
|
$getBlock(hash: string): Promise<IEsploraApi.Block> {
|
2023-08-05 13:08:47 +09:00
|
|
|
return this.failoverRouter.$get<IEsploraApi.Block>('/block/' + hash);
|
2020-12-28 04:47:22 +07:00
|
|
|
}
|
|
|
|
|
2022-11-29 11:37:51 +09:00
|
|
|
$getRawBlock(hash: string): Promise<Buffer> {
|
2023-08-05 13:08:47 +09:00
|
|
|
return this.failoverRouter.$get<any>('/block/' + hash + '/raw', 'arraybuffer')
|
2022-11-29 11:37:51 +09:00
|
|
|
.then((response) => { return Buffer.from(response.data); });
|
2022-07-25 14:54:00 -03:00
|
|
|
}
|
|
|
|
|
2020-12-28 04:47:22 +07:00
|
|
|
$getAddress(address: string): Promise<IEsploraApi.Address> {
|
|
|
|
throw new Error('Method getAddress not implemented.');
|
|
|
|
}
|
|
|
|
|
|
|
|
$getAddressTransactions(address: string, txId?: string): Promise<IEsploraApi.Transaction[]> {
|
2023-07-22 17:51:45 +09:00
|
|
|
throw new Error('Method getAddressTransactions not implemented.');
|
|
|
|
}
|
|
|
|
|
|
|
|
$getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash> {
|
2023-07-23 13:55:27 +09:00
|
|
|
throw new Error('Method getScriptHash not implemented.');
|
2023-07-22 17:51:45 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
$getScriptHashTransactions(scripthash: string, txId?: string): Promise<IEsploraApi.Transaction[]> {
|
2023-07-23 13:55:27 +09:00
|
|
|
throw new Error('Method getScriptHashTransactions not implemented.');
|
2020-12-28 04:47:22 +07:00
|
|
|
}
|
|
|
|
|
2021-01-11 01:51:57 +07:00
|
|
|
$getAddressPrefix(prefix: string): string[] {
|
|
|
|
throw new Error('Method not implemented.');
|
|
|
|
}
|
2021-09-26 22:18:44 +04:00
|
|
|
|
|
|
|
$sendRawTransaction(rawTransaction: string): Promise<string> {
|
|
|
|
throw new Error('Method not implemented.');
|
|
|
|
}
|
2022-01-15 22:09:04 +04:00
|
|
|
|
2024-03-23 11:27:28 +00:00
|
|
|
$testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]> {
|
|
|
|
throw new Error('Method not implemented.');
|
|
|
|
}
|
|
|
|
|
2022-07-06 11:58:06 +02:00
|
|
|
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
|
2023-08-05 13:08:47 +09:00
|
|
|
return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout);
|
2022-07-06 11:58:06 +02:00
|
|
|
}
|
|
|
|
|
2022-06-22 23:34:44 +02:00
|
|
|
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
|
2023-08-05 13:08:47 +09:00
|
|
|
return this.failoverRouter.$get<IEsploraApi.Outspend[]>('/tx/' + txId + '/outspends');
|
2022-06-22 23:34:44 +02:00
|
|
|
}
|
|
|
|
|
2023-08-16 02:25:48 +09:00
|
|
|
async $getBatchedOutspends(txids: string[]): Promise<IEsploraApi.Outspend[][]> {
|
2023-11-12 06:51:48 +00:00
|
|
|
throw new Error('Method not implemented.');
|
2022-01-15 22:09:04 +04:00
|
|
|
}
|
2023-08-05 13:08:47 +09:00
|
|
|
|
2023-08-17 02:42:59 +09:00
|
|
|
async $getBatchedOutspendsInternal(txids: string[]): Promise<IEsploraApi.Outspend[][]> {
|
2023-08-28 16:52:28 +09:00
|
|
|
return this.failoverRouter.$post<IEsploraApi.Outspend[][]>('/internal/txs/outspends/by-txid', txids, 'json');
|
2023-08-18 02:47:32 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
async $getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]> {
|
2023-08-28 16:52:28 +09:00
|
|
|
return this.failoverRouter.$post<IEsploraApi.Outspend[]>('/internal/txs/outspends/by-outpoint', outpoints.map(out => `${out.txid}:${out.vout}`), 'json');
|
2023-08-17 02:42:59 +09:00
|
|
|
}
|
|
|
|
|
2023-08-05 13:08:47 +09:00
|
|
|
public startHealthChecks(): void {
|
|
|
|
this.failoverRouter.startHealthChecks();
|
|
|
|
}
|
2024-03-03 20:29:54 +00:00
|
|
|
|
|
|
|
public getHealthStatus(): HealthCheckHost[] {
|
2024-03-04 18:29:50 +00:00
|
|
|
if (config.MEMPOOL.OFFICIAL) {
|
|
|
|
return this.failoverRouter.sortHosts().map(host => ({
|
|
|
|
host: host.host,
|
|
|
|
active: host === this.failoverRouter.activeHost,
|
|
|
|
rtt: host.rtt,
|
|
|
|
latestHeight: host.latestHeight || 0,
|
|
|
|
socket: !!host.socket,
|
|
|
|
outOfSync: !!host.outOfSync,
|
|
|
|
unreachable: !!host.unreachable,
|
|
|
|
checked: !!host.checked,
|
2024-03-06 19:34:12 +00:00
|
|
|
lastChecked: host.lastChecked || 0,
|
2024-03-04 18:29:50 +00:00
|
|
|
}));
|
|
|
|
} else {
|
|
|
|
return [];
|
|
|
|
}
|
2024-03-03 20:29:54 +00:00
|
|
|
}
|
2020-12-28 04:47:22 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
export default ElectrsApi;
|