Merge branch 'master' into hunicus/footer-refinement-exp
This commit is contained in:
		
						commit
						db90e77a32
					
				
							
								
								
									
										2
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							@ -1,6 +1,7 @@
 | 
			
		||||
version: 2
 | 
			
		||||
updates:
 | 
			
		||||
  - package-ecosystem: npm
 | 
			
		||||
    versioning-strategy: increase
 | 
			
		||||
    directory: "/backend"
 | 
			
		||||
    schedule:
 | 
			
		||||
      interval: daily
 | 
			
		||||
@ -14,6 +15,7 @@ updates:
 | 
			
		||||
 | 
			
		||||
  - package-ecosystem: npm
 | 
			
		||||
    directory: "/frontend"
 | 
			
		||||
    versioning-strategy: increase
 | 
			
		||||
    schedule:
 | 
			
		||||
      interval: daily
 | 
			
		||||
    open-pull-requests-limit: 10
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@ -47,7 +47,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - name: Unit Tests
 | 
			
		||||
        if: ${{ matrix.flavor == 'dev'}}
 | 
			
		||||
        run: npm run test
 | 
			
		||||
        run: npm run test:ci
 | 
			
		||||
        working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend
 | 
			
		||||
 | 
			
		||||
      - name: Build
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/cypress.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/cypress.yml
									
									
									
									
										vendored
									
									
								
							@ -38,7 +38,7 @@ jobs:
 | 
			
		||||
      - name: Setup node
 | 
			
		||||
        uses: actions/setup-node@v3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 16.15.0
 | 
			
		||||
          node-version: 18
 | 
			
		||||
          cache: "npm"
 | 
			
		||||
          cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -50,7 +50,8 @@
 | 
			
		||||
  "ESPLORA": {
 | 
			
		||||
    "REST_API_URL": "http://127.0.0.1:3000",
 | 
			
		||||
    "UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet",
 | 
			
		||||
    "RETRY_UNIX_SOCKET_AFTER": 30000
 | 
			
		||||
    "RETRY_UNIX_SOCKET_AFTER": 30000,
 | 
			
		||||
    "FALLBACK": []
 | 
			
		||||
  },
 | 
			
		||||
  "SECOND_CORE_RPC": {
 | 
			
		||||
    "HOST": "127.0.0.1",
 | 
			
		||||
 | 
			
		||||
@ -31,6 +31,7 @@
 | 
			
		||||
    "reindex-updated-pools": "npm run start-production --update-pools",
 | 
			
		||||
    "reindex-all-blocks": "npm run start-production --update-pools --reindex-blocks",
 | 
			
		||||
    "test": "./node_modules/.bin/jest --coverage",
 | 
			
		||||
    "test:ci": "CI=true ./node_modules/.bin/jest --coverage",
 | 
			
		||||
    "lint": "./node_modules/.bin/eslint . --ext .ts",
 | 
			
		||||
    "lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix",
 | 
			
		||||
    "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"",
 | 
			
		||||
 | 
			
		||||
@ -51,7 +51,8 @@
 | 
			
		||||
  "ESPLORA": {
 | 
			
		||||
    "REST_API_URL": "__ESPLORA_REST_API_URL__",
 | 
			
		||||
    "UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
 | 
			
		||||
    "RETRY_UNIX_SOCKET_AFTER": 888
 | 
			
		||||
    "RETRY_UNIX_SOCKET_AFTER": 888,
 | 
			
		||||
    "FALLBACK": []
 | 
			
		||||
  },
 | 
			
		||||
  "SECOND_CORE_RPC": {
 | 
			
		||||
    "HOST": "__SECOND_CORE_RPC_HOST__",
 | 
			
		||||
 | 
			
		||||
@ -52,7 +52,12 @@ describe('Mempool Backend Config', () => {
 | 
			
		||||
 | 
			
		||||
      expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
 | 
			
		||||
 | 
			
		||||
      expect(config.ESPLORA).toStrictEqual({ REST_API_URL: 'http://127.0.0.1:3000', UNIX_SOCKET_PATH: null, RETRY_UNIX_SOCKET_AFTER: 30000 });
 | 
			
		||||
      expect(config.ESPLORA).toStrictEqual({
 | 
			
		||||
        REST_API_URL: 'http://127.0.0.1:3000',
 | 
			
		||||
        UNIX_SOCKET_PATH: null,
 | 
			
		||||
        RETRY_UNIX_SOCKET_AFTER: 30000,
 | 
			
		||||
        FALLBACK: [],
 | 
			
		||||
       });
 | 
			
		||||
 | 
			
		||||
      expect(config.CORE_RPC).toStrictEqual({
 | 
			
		||||
        HOST: '127.0.0.1',
 | 
			
		||||
@ -181,7 +186,9 @@ describe('Mempool Backend Config', () => {
 | 
			
		||||
        for (const [key, value] of Object.entries(jsonObj)) {
 | 
			
		||||
          // We have a few cases where we can't follow the pattern
 | 
			
		||||
          if (root === 'MEMPOOL' && key === 'HTTP_PORT') {
 | 
			
		||||
            console.log('skipping check for MEMPOOL_HTTP_PORT');
 | 
			
		||||
            if (process.env.CI) {
 | 
			
		||||
              console.log('skipping check for MEMPOOL_HTTP_PORT');
 | 
			
		||||
            }
 | 
			
		||||
            continue;
 | 
			
		||||
          }
 | 
			
		||||
          switch (typeof value) {
 | 
			
		||||
@ -203,13 +210,17 @@ describe('Mempool Backend Config', () => {
 | 
			
		||||
              //The string used as the default value, to be checked as a regex, i.e, __MEMPOOL_ENABLED__=${MEMPOOL_ENABLED:=(.*)}
 | 
			
		||||
              const defaultEntry = replaceStr + '=' + '\\${' + envVarStr + ':=(.*)' + '}';
 | 
			
		||||
 | 
			
		||||
              console.log(`looking for ${defaultEntry} in the start.sh script`);
 | 
			
		||||
              if (process.env.CI) {
 | 
			
		||||
                console.log(`looking for ${defaultEntry} in the start.sh script`);
 | 
			
		||||
              }
 | 
			
		||||
              const re = new RegExp(defaultEntry);
 | 
			
		||||
              expect(startSh).toMatch(re);
 | 
			
		||||
 | 
			
		||||
              //The string that actually replaces the values in the config file
 | 
			
		||||
              const sedStr = 'sed -i "s!' + replaceStr + '!${' + replaceStr + '}!g" mempool-config.json';
 | 
			
		||||
              console.log(`looking for ${sedStr} in the start.sh script`);
 | 
			
		||||
              if (process.env.CI) {
 | 
			
		||||
                console.log(`looking for ${sedStr} in the start.sh script`);
 | 
			
		||||
              }
 | 
			
		||||
              expect(startSh).toContain(sedStr);
 | 
			
		||||
              break;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -23,6 +23,8 @@ export interface AbstractBitcoinApi {
 | 
			
		||||
  $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
 | 
			
		||||
  $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
 | 
			
		||||
  $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
 | 
			
		||||
 | 
			
		||||
  startHealthChecks(): void;
 | 
			
		||||
}
 | 
			
		||||
export interface BitcoinRpcCredentials {
 | 
			
		||||
  host: string;
 | 
			
		||||
 | 
			
		||||
@ -355,6 +355,7 @@ class BitcoinApi implements AbstractBitcoinApi {
 | 
			
		||||
    return transaction;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public startHealthChecks(): void {};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default BitcoinApi;
 | 
			
		||||
 | 
			
		||||
@ -1,135 +1,260 @@
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import axios, { AxiosRequestConfig } from 'axios';
 | 
			
		||||
import axios, { AxiosResponse } from 'axios';
 | 
			
		||||
import http from 'http';
 | 
			
		||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
 | 
			
		||||
import { IEsploraApi } from './esplora-api.interface';
 | 
			
		||||
import logger from '../../logger';
 | 
			
		||||
 | 
			
		||||
const axiosConnection = axios.create({
 | 
			
		||||
  httpAgent: new http.Agent({ keepAlive: true, })
 | 
			
		||||
});
 | 
			
		||||
interface FailoverHost {
 | 
			
		||||
  host: string,
 | 
			
		||||
  rtts: number[],
 | 
			
		||||
  rtt: number
 | 
			
		||||
  failures: number,
 | 
			
		||||
  socket?: boolean,
 | 
			
		||||
  outOfSync?: boolean,
 | 
			
		||||
  unreachable?: boolean,
 | 
			
		||||
  preferred?: boolean,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class ElectrsApi implements AbstractBitcoinApi {
 | 
			
		||||
  private axiosConfigWithUnixSocket: AxiosRequestConfig = config.ESPLORA.UNIX_SOCKET_PATH ? {
 | 
			
		||||
    socketPath: config.ESPLORA.UNIX_SOCKET_PATH,
 | 
			
		||||
    timeout: 10000,
 | 
			
		||||
  } : {
 | 
			
		||||
    timeout: 10000,
 | 
			
		||||
  };
 | 
			
		||||
  private axiosConfigTcpSocketOnly: AxiosRequestConfig = {
 | 
			
		||||
    timeout: 10000,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  unixSocketRetryTimeout;
 | 
			
		||||
  activeAxiosConfig;
 | 
			
		||||
class FailoverRouter {
 | 
			
		||||
  activeHost: FailoverHost;
 | 
			
		||||
  fallbackHost: FailoverHost;
 | 
			
		||||
  hosts: FailoverHost[];
 | 
			
		||||
  multihost: boolean;
 | 
			
		||||
  pollInterval: number = 60000;
 | 
			
		||||
  pollTimer: NodeJS.Timeout | null = null;
 | 
			
		||||
  pollConnection = axios.create();
 | 
			
		||||
  requestConnection = axios.create({
 | 
			
		||||
    httpAgent: new http.Agent({ keepAlive: true })
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.activeAxiosConfig = this.axiosConfigWithUnixSocket;
 | 
			
		||||
    // setup list of hosts
 | 
			
		||||
    this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => {
 | 
			
		||||
      return {
 | 
			
		||||
        host: domain,
 | 
			
		||||
        rtts: [],
 | 
			
		||||
        rtt: Infinity,
 | 
			
		||||
        failures: 0,
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
    this.activeHost = {
 | 
			
		||||
      host: config.ESPLORA.UNIX_SOCKET_PATH || config.ESPLORA.REST_API_URL,
 | 
			
		||||
      rtts: [],
 | 
			
		||||
      rtt: 0,
 | 
			
		||||
      failures: 0,
 | 
			
		||||
      socket: !!config.ESPLORA.UNIX_SOCKET_PATH,
 | 
			
		||||
      preferred: true,
 | 
			
		||||
    };
 | 
			
		||||
    this.fallbackHost = this.activeHost;
 | 
			
		||||
    this.hosts.unshift(this.activeHost);
 | 
			
		||||
    this.multihost = this.hosts.length > 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fallbackToTcpSocket() {
 | 
			
		||||
    if (!this.unixSocketRetryTimeout) {
 | 
			
		||||
      logger.err(`Unable to connect to esplora unix socket. Falling back to tcp socket. Retrying unix socket in ${config.ESPLORA.RETRY_UNIX_SOCKET_AFTER / 1000} seconds`);
 | 
			
		||||
      // Retry the unix socket after a few seconds
 | 
			
		||||
      this.unixSocketRetryTimeout = setTimeout(() => {
 | 
			
		||||
        logger.info(`Retrying to use unix socket for esplora now (applied for the next query)`);
 | 
			
		||||
        this.activeAxiosConfig = this.axiosConfigWithUnixSocket;
 | 
			
		||||
        this.unixSocketRetryTimeout = undefined;
 | 
			
		||||
      }, config.ESPLORA.RETRY_UNIX_SOCKET_AFTER);
 | 
			
		||||
  public startHealthChecks(): void {
 | 
			
		||||
    // use axios interceptors to measure request rtt
 | 
			
		||||
    this.pollConnection.interceptors.request.use((config) => {
 | 
			
		||||
      config['meta'] = { startTime: Date.now() };
 | 
			
		||||
      return config;
 | 
			
		||||
    });
 | 
			
		||||
    this.pollConnection.interceptors.response.use((response) => {
 | 
			
		||||
      response.config['meta'].rtt = Date.now() - response.config['meta'].startTime;
 | 
			
		||||
      return response;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (this.multihost) {
 | 
			
		||||
      this.pollHosts();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // start polling hosts to measure availability & rtt
 | 
			
		||||
  private async pollHosts(): Promise<void> {
 | 
			
		||||
    if (this.pollTimer) {
 | 
			
		||||
      clearTimeout(this.pollTimer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Use the TCP socket (reach a different esplora instance through nginx)
 | 
			
		||||
    this.activeAxiosConfig = this.axiosConfigTcpSocketOnly;
 | 
			
		||||
    const results = await Promise.allSettled(this.hosts.map(async (host) => {
 | 
			
		||||
      if (host.socket) {
 | 
			
		||||
        return this.pollConnection.get<number>('/blocks/tip/height', { socketPath: host.host, timeout: 2000 });
 | 
			
		||||
      } else {
 | 
			
		||||
        return this.pollConnection.get<number>(host.host + '/blocks/tip/height', { timeout: 2000 });
 | 
			
		||||
      }
 | 
			
		||||
    }));
 | 
			
		||||
    const maxHeight = results.reduce((max, result) => Math.max(max, result.status === 'fulfilled' ? result.value?.data || 0 : 0), 0);
 | 
			
		||||
 | 
			
		||||
    // update rtts & sync status
 | 
			
		||||
    for (let i = 0; i < results.length; i++) {
 | 
			
		||||
      const host = this.hosts[i];
 | 
			
		||||
      const result = results[i].status === 'fulfilled' ? (results[i] as PromiseFulfilledResult<AxiosResponse<number, any>>).value : null;
 | 
			
		||||
      if (result) {
 | 
			
		||||
        const height = result.data;
 | 
			
		||||
        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;
 | 
			
		||||
        if (height == null || isNaN(height) || (maxHeight - height > 2)) {
 | 
			
		||||
          host.outOfSync = true;
 | 
			
		||||
        } else {
 | 
			
		||||
          host.outOfSync = false;
 | 
			
		||||
        }
 | 
			
		||||
        host.unreachable = false;
 | 
			
		||||
      } else {
 | 
			
		||||
        host.unreachable = true;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.sortHosts();
 | 
			
		||||
 | 
			
		||||
    logger.debug(`Tomahawk ranking: ${this.hosts.map(host => '\navg rtt ' + Math.round(host.rtt).toString().padStart(5, ' ') + ' | reachable? ' + (!host.unreachable || false).toString().padStart(5, ' ') + ' | in sync? ' + (!host.outOfSync || false).toString().padStart(5, ' ') + ` | ${host.host}`).join('')}`);
 | 
			
		||||
 | 
			
		||||
    // 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 !== this.hosts[0] && this.hosts[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (this.hosts[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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.pollTimer = setTimeout(() => { this.pollHosts(); }, this.pollInterval);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $queryWrapper<T>(url, responseType = 'json'): Promise<T> {
 | 
			
		||||
    return axiosConnection.get<T>(url, { ...this.activeAxiosConfig, responseType: responseType })
 | 
			
		||||
      .then((response) => response.data)
 | 
			
		||||
  // sort hosts by connection quality, and update default fallback
 | 
			
		||||
  private sortHosts(): void {
 | 
			
		||||
    // sort by connection quality
 | 
			
		||||
    this.hosts.sort((a, b) => {
 | 
			
		||||
      if ((a.unreachable || a.outOfSync) === (b.unreachable || b.outOfSync)) {
 | 
			
		||||
        if  (a.preferred === b.preferred) {
 | 
			
		||||
          // lower rtt is best
 | 
			
		||||
          return a.rtt - b.rtt;
 | 
			
		||||
        } 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;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    if (this.hosts.length > 1 && this.hosts[0] === this.activeHost) {
 | 
			
		||||
      this.fallbackHost = this.hosts[1];
 | 
			
		||||
    } else {
 | 
			
		||||
      this.fallbackHost = this.hosts[0];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // depose the active host and choose the next best replacement
 | 
			
		||||
  private electHost(): void {
 | 
			
		||||
    this.activeHost.outOfSync = true;
 | 
			
		||||
    this.activeHost.failures = 0;
 | 
			
		||||
    this.sortHosts();
 | 
			
		||||
    this.activeHost = this.hosts[0];
 | 
			
		||||
    logger.warn(`Switching esplora host to ${this.activeHost.host}`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private addFailure(host: FailoverHost): FailoverHost {
 | 
			
		||||
    host.failures++;
 | 
			
		||||
    if (host.failures > 5 && this.multihost) {
 | 
			
		||||
      logger.warn(`Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative`);
 | 
			
		||||
      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) {
 | 
			
		||||
      axiosConfig = { socketPath: host.host, timeout: 10000, responseType };
 | 
			
		||||
      url = path;
 | 
			
		||||
    } else {
 | 
			
		||||
      axiosConfig = { timeout: 10000, responseType };
 | 
			
		||||
      url = host.host + path;
 | 
			
		||||
    }
 | 
			
		||||
    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; })
 | 
			
		||||
      .catch((e) => {
 | 
			
		||||
        if (e?.code === 'ECONNREFUSED') {
 | 
			
		||||
          this.fallbackToTcpSocket();
 | 
			
		||||
        let fallbackHost = this.fallbackHost;
 | 
			
		||||
        if (e?.response?.status !== 404) {
 | 
			
		||||
          logger.warn(`esplora request failed ${e?.response?.status || 500} ${host.host}${path}`);
 | 
			
		||||
          fallbackHost = this.addFailure(host);
 | 
			
		||||
        }
 | 
			
		||||
        if (retry && e?.code === 'ECONNREFUSED' && this.multihost) {
 | 
			
		||||
          // Retry immediately
 | 
			
		||||
          return axiosConnection.get<T>(url, this.activeAxiosConfig)
 | 
			
		||||
            .then((response) => response.data)
 | 
			
		||||
            .catch((e) => {
 | 
			
		||||
              logger.warn(`Cannot query esplora through the unix socket nor the tcp socket. Exception ${e}`);
 | 
			
		||||
              throw e;
 | 
			
		||||
            });
 | 
			
		||||
          return this.$query(method, path, data, responseType, fallbackHost, false);
 | 
			
		||||
        } else {
 | 
			
		||||
          throw e;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $postWrapper<T>(url, body, responseType = 'json', params: any = undefined): Promise<T> {
 | 
			
		||||
    return axiosConnection.post<T>(url, body, { ...this.activeAxiosConfig, responseType: responseType, params })
 | 
			
		||||
      .then((response) => response.data)
 | 
			
		||||
      .catch((e) => {
 | 
			
		||||
        if (e?.code === 'ECONNREFUSED') {
 | 
			
		||||
          this.fallbackToTcpSocket();
 | 
			
		||||
          // Retry immediately
 | 
			
		||||
          return axiosConnection.post<T>(url, body, this.activeAxiosConfig)
 | 
			
		||||
            .then((response) => response.data)
 | 
			
		||||
            .catch((e) => {
 | 
			
		||||
              logger.warn(`Cannot query esplora through the unix socket nor the tcp socket. Exception ${e}`);
 | 
			
		||||
              throw e;
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
          throw e;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
  public async $get<T>(path, responseType = 'json'): Promise<T> {
 | 
			
		||||
    return this.$query<T>('get', path, null, responseType);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $post<T>(path, data: any, responseType = 'json'): Promise<T> {
 | 
			
		||||
    return this.$query<T>('post', path, data, responseType);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class ElectrsApi implements AbstractBitcoinApi {
 | 
			
		||||
  private failoverRouter = new FailoverRouter();
 | 
			
		||||
 | 
			
		||||
  $getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
 | 
			
		||||
    return this.$queryWrapper<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids');
 | 
			
		||||
    return this.failoverRouter.$get<IEsploraApi.Transaction['txid'][]>('/mempool/txids');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> {
 | 
			
		||||
    return this.$queryWrapper<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId);
 | 
			
		||||
    return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> {
 | 
			
		||||
    return this.$postWrapper<IEsploraApi.Transaction[]>(config.ESPLORA.REST_API_URL + '/mempool/txs', txids, 'json');
 | 
			
		||||
    return this.failoverRouter.$post<IEsploraApi.Transaction[]>('/mempool/txs', txids, 'json');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $getAllMempoolTransactions(lastSeenTxid?: string): Promise<IEsploraApi.Transaction[]> {
 | 
			
		||||
    return this.$queryWrapper<IEsploraApi.Transaction[]>(config.ESPLORA.REST_API_URL + '/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''));
 | 
			
		||||
    return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getTransactionHex(txId: string): Promise<string> {
 | 
			
		||||
    return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex');
 | 
			
		||||
    return this.failoverRouter.$get<string>('/tx/' + txId + '/hex');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHeightTip(): Promise<number> {
 | 
			
		||||
    return this.$queryWrapper<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height');
 | 
			
		||||
    return this.failoverRouter.$get<number>('/blocks/tip/height');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHashTip(): Promise<string> {
 | 
			
		||||
    return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/blocks/tip/hash');
 | 
			
		||||
    return this.failoverRouter.$get<string>('/blocks/tip/hash');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getTxIdsForBlock(hash: string): Promise<string[]> {
 | 
			
		||||
    return this.$queryWrapper<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids');
 | 
			
		||||
    return this.failoverRouter.$get<string[]>('/block/' + hash + '/txids');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
 | 
			
		||||
    return this.$queryWrapper<IEsploraApi.Transaction[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txs');
 | 
			
		||||
    return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/block/' + hash + '/txs');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHash(height: number): Promise<string> {
 | 
			
		||||
    return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height);
 | 
			
		||||
    return this.failoverRouter.$get<string>('/block-height/' + height);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHeader(hash: string): Promise<string> {
 | 
			
		||||
    return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header');
 | 
			
		||||
    return this.failoverRouter.$get<string>('/block/' + hash + '/header');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlock(hash: string): Promise<IEsploraApi.Block> {
 | 
			
		||||
    return this.$queryWrapper<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash);
 | 
			
		||||
    return this.failoverRouter.$get<IEsploraApi.Block>('/block/' + hash);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawBlock(hash: string): Promise<Buffer> {
 | 
			
		||||
    return this.$queryWrapper<any>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", 'arraybuffer')
 | 
			
		||||
    return this.failoverRouter.$get<any>('/block/' + hash + '/raw', 'arraybuffer')
 | 
			
		||||
      .then((response) => { return Buffer.from(response.data); });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -158,11 +283,11 @@ class ElectrsApi implements AbstractBitcoinApi {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
 | 
			
		||||
    return this.$queryWrapper<IEsploraApi.Outspend>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout);
 | 
			
		||||
    return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
 | 
			
		||||
    return this.$queryWrapper<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends');
 | 
			
		||||
    return this.failoverRouter.$get<IEsploraApi.Outspend[]>('/tx/' + txId + '/outspends');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
 | 
			
		||||
@ -173,6 +298,10 @@ class ElectrsApi implements AbstractBitcoinApi {
 | 
			
		||||
    }
 | 
			
		||||
    return outspends;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public startHealthChecks(): void {
 | 
			
		||||
    this.failoverRouter.startHealthChecks();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ElectrsApi;
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@ import PricesRepository from '../../repositories/PricesRepository';
 | 
			
		||||
class MiningRoutes {
 | 
			
		||||
  public initRoutes(app: Application) {
 | 
			
		||||
    app
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools', this.$listPools)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/:interval', this.$getPools)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/hashrate', this.$getPoolHistoricalHashrate)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks', this.$getPoolBlocks)
 | 
			
		||||
@ -41,6 +42,10 @@ class MiningRoutes {
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
 | 
			
		||||
      if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
 | 
			
		||||
        res.status(400).send('Prices are not available on testnets.');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (req.query.timestamp) {
 | 
			
		||||
        res.status(200).send(await PricesRepository.$getNearestHistoricalPrice(
 | 
			
		||||
          parseInt(<string>req.query.timestamp ?? 0, 10)
 | 
			
		||||
@ -88,6 +93,29 @@ class MiningRoutes {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $listPools(req: Request, res: Response): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
 | 
			
		||||
      const pools = await mining.$listPools();
 | 
			
		||||
      if (!pools) {
 | 
			
		||||
        res.status(500).end();
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      res.header('X-total-count', pools.length.toString());
 | 
			
		||||
      if (pools.length === 0) {
 | 
			
		||||
        res.status(204).send();
 | 
			
		||||
      } else {
 | 
			
		||||
        res.json(pools);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getPools(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const stats = await mining.$getPoolsStats(req.params.interval);
 | 
			
		||||
 | 
			
		||||
@ -26,7 +26,7 @@ class Mining {
 | 
			
		||||
  /**
 | 
			
		||||
   * Get historical blocks health
 | 
			
		||||
   */
 | 
			
		||||
   public async $getBlocksHealthHistory(interval: string | null = null): Promise<any> {
 | 
			
		||||
  public async $getBlocksHealthHistory(interval: string | null = null): Promise<any> {
 | 
			
		||||
    return await BlocksAuditsRepository.$getBlocksHealthHistory(
 | 
			
		||||
      this.getTimeRange(interval),
 | 
			
		||||
      Common.getSqlInterval(interval)
 | 
			
		||||
@ -56,7 +56,7 @@ class Mining {
 | 
			
		||||
  /**
 | 
			
		||||
   * Get historical block fee rates percentiles
 | 
			
		||||
   */
 | 
			
		||||
   public async $getHistoricalBlockFeeRates(interval: string | null = null): Promise<any> {
 | 
			
		||||
  public async $getHistoricalBlockFeeRates(interval: string | null = null): Promise<any> {
 | 
			
		||||
    return await BlocksRepository.$getHistoricalBlockFeeRates(
 | 
			
		||||
      this.getTimeRange(interval),
 | 
			
		||||
      Common.getSqlInterval(interval)
 | 
			
		||||
@ -66,7 +66,7 @@ class Mining {
 | 
			
		||||
  /**
 | 
			
		||||
   * Get historical block sizes
 | 
			
		||||
   */
 | 
			
		||||
   public async $getHistoricalBlockSizes(interval: string | null = null): Promise<any> {
 | 
			
		||||
  public async $getHistoricalBlockSizes(interval: string | null = null): Promise<any> {
 | 
			
		||||
    return await BlocksRepository.$getHistoricalBlockSizes(
 | 
			
		||||
      this.getTimeRange(interval),
 | 
			
		||||
      Common.getSqlInterval(interval)
 | 
			
		||||
@ -76,7 +76,7 @@ class Mining {
 | 
			
		||||
  /**
 | 
			
		||||
   * Get historical block weights
 | 
			
		||||
   */
 | 
			
		||||
   public async $getHistoricalBlockWeights(interval: string | null = null): Promise<any> {
 | 
			
		||||
  public async $getHistoricalBlockWeights(interval: string | null = null): Promise<any> {
 | 
			
		||||
    return await BlocksRepository.$getHistoricalBlockWeights(
 | 
			
		||||
      this.getTimeRange(interval),
 | 
			
		||||
      Common.getSqlInterval(interval)
 | 
			
		||||
@ -595,6 +595,20 @@ class Mining {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * List existing mining pools
 | 
			
		||||
   */
 | 
			
		||||
  public async $listPools(): Promise<{name: string, slug: string, unique_id: number}[] | null> {
 | 
			
		||||
    const [rows] = await database.query(`
 | 
			
		||||
      SELECT
 | 
			
		||||
        name,
 | 
			
		||||
        slug,
 | 
			
		||||
        unique_id
 | 
			
		||||
      FROM pools`
 | 
			
		||||
    );
 | 
			
		||||
    return rows as {name: string, slug: string, unique_id: number}[];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getDateMidnight(date: Date): Date {
 | 
			
		||||
    date.setUTCHours(0);
 | 
			
		||||
    date.setUTCMinutes(0);
 | 
			
		||||
 | 
			
		||||
@ -198,18 +198,14 @@ class WebsocketHandler {
 | 
			
		||||
                matchedAddress = matchedAddress.toLowerCase();
 | 
			
		||||
              }
 | 
			
		||||
              if (/^04[a-fA-F0-9]{128}$/.test(parsedMessage['track-address'])) {
 | 
			
		||||
                client['track-address'] = null;
 | 
			
		||||
                client['track-scriptpubkey'] = '41' + matchedAddress + 'ac';
 | 
			
		||||
              } else if (/^|(02|03)[a-fA-F0-9]{64}$/.test(parsedMessage['track-address'])) {
 | 
			
		||||
                client['track-address'] = null;
 | 
			
		||||
                client['track-scriptpubkey'] = '21' + matchedAddress + 'ac';
 | 
			
		||||
                client['track-address'] = '41' + matchedAddress + 'ac';
 | 
			
		||||
              } else if (/^(02|03)[a-fA-F0-9]{64}$/.test(parsedMessage['track-address'])) {
 | 
			
		||||
                client['track-address'] = '21' + matchedAddress + 'ac';
 | 
			
		||||
              } else {
 | 
			
		||||
                client['track-address'] = matchedAddress;
 | 
			
		||||
                client['track-scriptpubkey'] = null;
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
              client['track-address'] = null;
 | 
			
		||||
              client['track-scriptpubkey'] = null;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
@ -488,6 +484,9 @@ class WebsocketHandler {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // pre-compute address transactions
 | 
			
		||||
    const addressCache = this.makeAddressCache(newTransactions);
 | 
			
		||||
 | 
			
		||||
    this.wss.clients.forEach(async (client) => {
 | 
			
		||||
      if (client.readyState !== WebSocket.OPEN) {
 | 
			
		||||
        return;
 | 
			
		||||
@ -527,78 +526,13 @@ class WebsocketHandler {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (client['track-address']) {
 | 
			
		||||
        const foundTransactions: TransactionExtended[] = [];
 | 
			
		||||
        const foundTransactions = Array.from(addressCache[client['track-address']]?.values() || []);
 | 
			
		||||
        // txs may be missing prevouts in non-esplora backends
 | 
			
		||||
        // so fetch the full transactions now
 | 
			
		||||
        const fullTransactions = (config.MEMPOOL.BACKEND !== 'esplora') ? await this.getFullTransactions(foundTransactions) : foundTransactions;
 | 
			
		||||
 | 
			
		||||
        for (const tx of newTransactions) {
 | 
			
		||||
          const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_address === client['track-address']);
 | 
			
		||||
          if (someVin) {
 | 
			
		||||
            if (config.MEMPOOL.BACKEND !== 'esplora') {
 | 
			
		||||
              try {
 | 
			
		||||
                const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
 | 
			
		||||
                foundTransactions.push(fullTx);
 | 
			
		||||
              } catch (e) {
 | 
			
		||||
                logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
              foundTransactions.push(tx);
 | 
			
		||||
            }
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          const someVout = tx.vout.some((vout) => vout.scriptpubkey_address === client['track-address']);
 | 
			
		||||
          if (someVout) {
 | 
			
		||||
            if (config.MEMPOOL.BACKEND !== 'esplora') {
 | 
			
		||||
              try {
 | 
			
		||||
                const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
 | 
			
		||||
                foundTransactions.push(fullTx);
 | 
			
		||||
              } catch (e) {
 | 
			
		||||
                logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
              foundTransactions.push(tx);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (foundTransactions.length) {
 | 
			
		||||
          response['address-transactions'] = JSON.stringify(foundTransactions);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (client['track-scriptpubkey']) {
 | 
			
		||||
        const foundTransactions: TransactionExtended[] = [];
 | 
			
		||||
 | 
			
		||||
        for (const tx of newTransactions) {
 | 
			
		||||
          const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scriptpubkey']);
 | 
			
		||||
          if (someVin) {
 | 
			
		||||
            if (config.MEMPOOL.BACKEND !== 'esplora') {
 | 
			
		||||
              try {
 | 
			
		||||
                const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
 | 
			
		||||
                foundTransactions.push(fullTx);
 | 
			
		||||
              } catch (e) {
 | 
			
		||||
                logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
              foundTransactions.push(tx);
 | 
			
		||||
            }
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          const someVout = tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scriptpubkey']);
 | 
			
		||||
          if (someVout) {
 | 
			
		||||
            if (config.MEMPOOL.BACKEND !== 'esplora') {
 | 
			
		||||
              try {
 | 
			
		||||
                const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
 | 
			
		||||
                foundTransactions.push(fullTx);
 | 
			
		||||
              } catch (e) {
 | 
			
		||||
                logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
              foundTransactions.push(tx);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (foundTransactions.length) {
 | 
			
		||||
          response['address-transactions'] = JSON.stringify(foundTransactions);
 | 
			
		||||
        if (fullTransactions.length) {
 | 
			
		||||
          response['address-transactions'] = JSON.stringify(fullTransactions);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -606,7 +540,6 @@ class WebsocketHandler {
 | 
			
		||||
        const foundTransactions: TransactionExtended[] = [];
 | 
			
		||||
 | 
			
		||||
        newTransactions.forEach((tx) => {
 | 
			
		||||
 | 
			
		||||
          if (client['track-asset'] === Common.nativeAssetId) {
 | 
			
		||||
            if (tx.vin.some((vin) => !!vin.is_pegin)) {
 | 
			
		||||
              foundTransactions.push(tx);
 | 
			
		||||
@ -805,6 +738,9 @@ class WebsocketHandler {
 | 
			
		||||
    const fees = feeApi.getRecommendedFee();
 | 
			
		||||
    const mempoolInfo = memPool.getMempoolInfo();
 | 
			
		||||
 | 
			
		||||
    // pre-compute address transactions
 | 
			
		||||
    const addressCache = this.makeAddressCache(transactions);
 | 
			
		||||
 | 
			
		||||
    // update init data
 | 
			
		||||
    this.updateSocketDataFields({
 | 
			
		||||
      'mempoolInfo': mempoolInfo,
 | 
			
		||||
@ -867,44 +803,7 @@ class WebsocketHandler {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (client['track-address']) {
 | 
			
		||||
        const foundTransactions: TransactionExtended[] = [];
 | 
			
		||||
 | 
			
		||||
        transactions.forEach((tx) => {
 | 
			
		||||
          if (tx.vin && tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_address === client['track-address'])) {
 | 
			
		||||
            foundTransactions.push(tx);
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          if (tx.vout && tx.vout.some((vout) => vout.scriptpubkey_address === client['track-address'])) {
 | 
			
		||||
            foundTransactions.push(tx);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (foundTransactions.length) {
 | 
			
		||||
          foundTransactions.forEach((tx) => {
 | 
			
		||||
            tx.status = {
 | 
			
		||||
              confirmed: true,
 | 
			
		||||
              block_height: block.height,
 | 
			
		||||
              block_hash: block.id,
 | 
			
		||||
              block_time: block.timestamp,
 | 
			
		||||
            };
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          response['block-transactions'] = JSON.stringify(foundTransactions);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (client['track-scriptpubkey']) {
 | 
			
		||||
        const foundTransactions: TransactionExtended[] = [];
 | 
			
		||||
 | 
			
		||||
        transactions.forEach((tx) => {
 | 
			
		||||
          if (tx.vin && tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scriptpubkey'])) {
 | 
			
		||||
            foundTransactions.push(tx);
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          if (tx.vout && tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scriptpubkey'])) {
 | 
			
		||||
            foundTransactions.push(tx);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
        const foundTransactions: TransactionExtended[] = Array.from(addressCache[client['track-address']]?.values() || []);
 | 
			
		||||
 | 
			
		||||
        if (foundTransactions.length) {
 | 
			
		||||
          foundTransactions.forEach((tx) => {
 | 
			
		||||
@ -982,6 +881,52 @@ class WebsocketHandler {
 | 
			
		||||
        + '}';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private makeAddressCache(transactions: MempoolTransactionExtended[]): { [address: string]: Set<MempoolTransactionExtended> } {
 | 
			
		||||
    const addressCache: { [address: string]: Set<MempoolTransactionExtended> } = {};
 | 
			
		||||
    for (const tx of transactions) {
 | 
			
		||||
      for (const vin of tx.vin) {
 | 
			
		||||
        if (vin?.prevout?.scriptpubkey_address) {
 | 
			
		||||
          if (!addressCache[vin.prevout.scriptpubkey_address]) {
 | 
			
		||||
            addressCache[vin.prevout.scriptpubkey_address] = new Set();
 | 
			
		||||
          }
 | 
			
		||||
          addressCache[vin.prevout.scriptpubkey_address].add(tx);
 | 
			
		||||
        }
 | 
			
		||||
        if (vin?.prevout?.scriptpubkey) {
 | 
			
		||||
          if (!addressCache[vin.prevout.scriptpubkey]) {
 | 
			
		||||
            addressCache[vin.prevout.scriptpubkey] = new Set();
 | 
			
		||||
          }
 | 
			
		||||
          addressCache[vin.prevout.scriptpubkey].add(tx);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      for (const vout of tx.vout) {
 | 
			
		||||
        if (vout?.scriptpubkey_address) {
 | 
			
		||||
          if (!addressCache[vout?.scriptpubkey_address]) {
 | 
			
		||||
            addressCache[vout?.scriptpubkey_address] = new Set();
 | 
			
		||||
          }
 | 
			
		||||
          addressCache[vout?.scriptpubkey_address].add(tx);
 | 
			
		||||
        }
 | 
			
		||||
        if (vout?.scriptpubkey) {
 | 
			
		||||
          if (!addressCache[vout.scriptpubkey]) {
 | 
			
		||||
            addressCache[vout.scriptpubkey] = new Set();
 | 
			
		||||
          }
 | 
			
		||||
          addressCache[vout.scriptpubkey].add(tx);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return addressCache;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getFullTransactions(transactions: MempoolTransactionExtended[]): Promise<MempoolTransactionExtended[]> {
 | 
			
		||||
    for (let i = 0; i < transactions.length; i++) {
 | 
			
		||||
      try {
 | 
			
		||||
        transactions[i] = await transactionUtils.$getMempoolTransactionExtended(transactions[i].txid, true);
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return transactions;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private printLogs(): void {
 | 
			
		||||
    if (this.wss) {
 | 
			
		||||
      const count = this.wss?.clients?.size || 0;
 | 
			
		||||
 | 
			
		||||
@ -44,6 +44,7 @@ interface IConfig {
 | 
			
		||||
    REST_API_URL: string;
 | 
			
		||||
    UNIX_SOCKET_PATH: string | void | null;
 | 
			
		||||
    RETRY_UNIX_SOCKET_AFTER: number;
 | 
			
		||||
    FALLBACK: string[];
 | 
			
		||||
  };
 | 
			
		||||
  LIGHTNING: {
 | 
			
		||||
    ENABLED: boolean;
 | 
			
		||||
@ -188,6 +189,7 @@ const defaults: IConfig = {
 | 
			
		||||
    'REST_API_URL': 'http://127.0.0.1:3000',
 | 
			
		||||
    'UNIX_SOCKET_PATH': null,
 | 
			
		||||
    'RETRY_UNIX_SOCKET_AFTER': 30000,
 | 
			
		||||
    'FALLBACK': [],
 | 
			
		||||
  },
 | 
			
		||||
  'ELECTRUM': {
 | 
			
		||||
    'HOST': '127.0.0.1',
 | 
			
		||||
 | 
			
		||||
@ -91,6 +91,10 @@ class Server {
 | 
			
		||||
  async startServer(worker = false): Promise<void> {
 | 
			
		||||
    logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
 | 
			
		||||
 | 
			
		||||
    if (config.MEMPOOL.BACKEND === 'esplora') {
 | 
			
		||||
      bitcoinApi.startHealthChecks();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (config.DATABASE.ENABLED) {
 | 
			
		||||
      await DB.checkDbConnection();
 | 
			
		||||
      try {
 | 
			
		||||
 | 
			
		||||
@ -51,7 +51,8 @@
 | 
			
		||||
  "ESPLORA": {
 | 
			
		||||
    "REST_API_URL": "__ESPLORA_REST_API_URL__",
 | 
			
		||||
    "UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
 | 
			
		||||
    "RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__
 | 
			
		||||
    "RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__,
 | 
			
		||||
    "FALLBACK": __ESPLORA_FALLBACK__,
 | 
			
		||||
  },
 | 
			
		||||
  "SECOND_CORE_RPC": {
 | 
			
		||||
    "HOST": "__SECOND_CORE_RPC_HOST__",
 | 
			
		||||
 | 
			
		||||
@ -42,9 +42,6 @@
 | 
			
		||||
// -- This will overwrite an existing command --
 | 
			
		||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
 | 
			
		||||
 | 
			
		||||
'use strict'
 | 
			
		||||
 | 
			
		||||
import 'cypress-wait-until';
 | 
			
		||||
import { PageIdleDetector } from './PageIdleDetector';
 | 
			
		||||
import { mockWebSocket } from './websocket';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,7 @@
 | 
			
		||||
// ***********************************************************
 | 
			
		||||
 | 
			
		||||
// When a command from ./commands is ready to use, import with `import './commands'` syntax
 | 
			
		||||
import 'cypress-wait-until';
 | 
			
		||||
import './commands';
 | 
			
		||||
import failOnConsoleError from 'cypress-fail-on-console-error';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
  "extends": "../tsconfig.json",
 | 
			
		||||
  "include": ["**/*.ts"],
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "types": ["cypress"],
 | 
			
		||||
    "types": ["cypress", "node", "cypress-wait-until"],
 | 
			
		||||
    "lib": ["es2015", "dom"],
 | 
			
		||||
    "allowJs": true,
 | 
			
		||||
    "noEmit": true,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										54
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										54
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -58,9 +58,10 @@
 | 
			
		||||
      },
 | 
			
		||||
      "optionalDependencies": {
 | 
			
		||||
        "@cypress/schematic": "^2.5.0",
 | 
			
		||||
        "cypress": "^12.17.1",
 | 
			
		||||
        "@types/cypress": "^1.1.3",
 | 
			
		||||
        "cypress": "^12.17.2",
 | 
			
		||||
        "cypress-fail-on-console-error": "~4.0.3",
 | 
			
		||||
        "cypress-wait-until": "^1.7.2",
 | 
			
		||||
        "cypress-wait-until": "^2.0.0",
 | 
			
		||||
        "mock-socket": "~9.2.1",
 | 
			
		||||
        "start-server-and-test": "~2.0.0"
 | 
			
		||||
      }
 | 
			
		||||
@ -3925,6 +3926,16 @@
 | 
			
		||||
        "@types/node": "*"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/cypress": {
 | 
			
		||||
      "version": "1.1.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/cypress/-/cypress-1.1.3.tgz",
 | 
			
		||||
      "integrity": "sha512-OXe0Gw8LeCflkG1oPgFpyrYWJmEKqYncBsD/J0r17r0ETx/TnIGDNLwXt/pFYSYuYTpzcq1q3g62M9DrfsBL4g==",
 | 
			
		||||
      "deprecated": "This is a stub types definition for cypress (https://cypress.io). cypress provides its own type definitions, so you don't need @types/cypress installed!",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "cypress": "*"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/eslint": {
 | 
			
		||||
      "version": "8.4.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz",
 | 
			
		||||
@ -6641,9 +6652,9 @@
 | 
			
		||||
      "peer": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/cypress": {
 | 
			
		||||
      "version": "12.17.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.17.1.tgz",
 | 
			
		||||
      "integrity": "sha512-eKfBgO6t8waEyhegL4gxD7tcI6uTCGttu+ZU7y9Hq8BlpMztd7iLeIF4AJFAnbZH1xjX+wwgg4cRKFNSvv3VWQ==",
 | 
			
		||||
      "version": "12.17.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.17.2.tgz",
 | 
			
		||||
      "integrity": "sha512-hxWAaWbqQBzzMuadSGSuQg5PDvIGOovm6xm0hIfpCVcORsCAj/gF2p0EvfnJ4f+jK2PCiDgP6D2eeE9/FK4Mjg==",
 | 
			
		||||
      "hasInstallScript": true,
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
@ -6710,10 +6721,14 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/cypress-wait-until": {
 | 
			
		||||
      "version": "1.7.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cypress-wait-until/-/cypress-wait-until-1.7.2.tgz",
 | 
			
		||||
      "integrity": "sha512-uZ+M8/MqRcpf+FII/UZrU7g1qYZ4aVlHcgyVopnladyoBrpoaMJ4PKZDrdOJ05H5RHbr7s9Tid635X3E+ZLU/Q==",
 | 
			
		||||
      "optional": true
 | 
			
		||||
      "version": "2.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cypress-wait-until/-/cypress-wait-until-2.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-ulUZyrWBn+OuC8oiQuGKAScDYfpaWnE3dEE/raUo64w4RHQxZrQ/iMIWT4ZjGMMPr3P+BFEALCRnjQeRqzZj6g==",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18.16.0",
 | 
			
		||||
        "npm": ">=9.5.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/cypress/node_modules/@types/node": {
 | 
			
		||||
      "version": "14.18.53",
 | 
			
		||||
@ -18862,6 +18877,15 @@
 | 
			
		||||
        "@types/node": "*"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "@types/cypress": {
 | 
			
		||||
      "version": "1.1.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/cypress/-/cypress-1.1.3.tgz",
 | 
			
		||||
      "integrity": "sha512-OXe0Gw8LeCflkG1oPgFpyrYWJmEKqYncBsD/J0r17r0ETx/TnIGDNLwXt/pFYSYuYTpzcq1q3g62M9DrfsBL4g==",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "cypress": "*"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "@types/eslint": {
 | 
			
		||||
      "version": "8.4.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz",
 | 
			
		||||
@ -20968,9 +20992,9 @@
 | 
			
		||||
      "peer": true
 | 
			
		||||
    },
 | 
			
		||||
    "cypress": {
 | 
			
		||||
      "version": "12.17.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.17.1.tgz",
 | 
			
		||||
      "integrity": "sha512-eKfBgO6t8waEyhegL4gxD7tcI6uTCGttu+ZU7y9Hq8BlpMztd7iLeIF4AJFAnbZH1xjX+wwgg4cRKFNSvv3VWQ==",
 | 
			
		||||
      "version": "12.17.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.17.2.tgz",
 | 
			
		||||
      "integrity": "sha512-hxWAaWbqQBzzMuadSGSuQg5PDvIGOovm6xm0hIfpCVcORsCAj/gF2p0EvfnJ4f+jK2PCiDgP6D2eeE9/FK4Mjg==",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "@cypress/request": "^2.88.11",
 | 
			
		||||
@ -21151,9 +21175,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "cypress-wait-until": {
 | 
			
		||||
      "version": "1.7.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cypress-wait-until/-/cypress-wait-until-1.7.2.tgz",
 | 
			
		||||
      "integrity": "sha512-uZ+M8/MqRcpf+FII/UZrU7g1qYZ4aVlHcgyVopnladyoBrpoaMJ4PKZDrdOJ05H5RHbr7s9Tid635X3E+ZLU/Q==",
 | 
			
		||||
      "version": "2.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cypress-wait-until/-/cypress-wait-until-2.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-ulUZyrWBn+OuC8oiQuGKAScDYfpaWnE3dEE/raUo64w4RHQxZrQ/iMIWT4ZjGMMPr3P+BFEALCRnjQeRqzZj6g==",
 | 
			
		||||
      "optional": true
 | 
			
		||||
    },
 | 
			
		||||
    "d": {
 | 
			
		||||
 | 
			
		||||
@ -110,9 +110,10 @@
 | 
			
		||||
  },
 | 
			
		||||
  "optionalDependencies": {
 | 
			
		||||
    "@cypress/schematic": "^2.5.0",
 | 
			
		||||
    "cypress": "^12.17.1",
 | 
			
		||||
    "@types/cypress": "^1.1.3",
 | 
			
		||||
    "cypress": "^12.17.2",
 | 
			
		||||
    "cypress-fail-on-console-error": "~4.0.3",
 | 
			
		||||
    "cypress-wait-until": "^1.7.2",
 | 
			
		||||
    "cypress-wait-until": "^2.0.0",
 | 
			
		||||
    "mock-socket": "~9.2.1",
 | 
			
		||||
    "start-server-and-test": "~2.0.0"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
 | 
			
		||||
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
 | 
			
		||||
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
 | 
			
		||||
  PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit } from '../interfaces/node-api.interface';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
import { Observable, of } from 'rxjs';
 | 
			
		||||
import { StateService } from './state.service';
 | 
			
		||||
import { WebsocketResponse } from '../interfaces/websocket.interface';
 | 
			
		||||
import { Outspend, Transaction } from '../interfaces/electrs.interface';
 | 
			
		||||
@ -312,6 +312,19 @@ export class ApiService {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getHistoricalPrice$(timestamp: number | undefined): Observable<Conversion> {
 | 
			
		||||
    if (this.stateService.isAnyTestnet()) {
 | 
			
		||||
      return of({
 | 
			
		||||
        prices: [],
 | 
			
		||||
        exchangeRates: {
 | 
			
		||||
          USDEUR: 0,
 | 
			
		||||
          USDGBP: 0,
 | 
			
		||||
          USDCAD: 0,
 | 
			
		||||
          USDCHF: 0,
 | 
			
		||||
          USDAUD: 0,
 | 
			
		||||
          USDJPY: 0,
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return this.httpClient.get<Conversion>(
 | 
			
		||||
      this.apiBaseUrl + this.apiBasePath + '/api/v1/historical-price' +
 | 
			
		||||
        (timestamp ? `?timestamp=${timestamp}` : '')
 | 
			
		||||
 | 
			
		||||
@ -339,6 +339,10 @@ export class StateService {
 | 
			
		||||
    return this.network === 'liquid' || this.network === 'liquidtestnet';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isAnyTestnet(): boolean {
 | 
			
		||||
    return ['testnet', 'signet', 'liquidtestnet'].includes(this.network);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  resetChainTip() {
 | 
			
		||||
    this.latestBlockHeight = -1;
 | 
			
		||||
    this.chainTip$.next(-1);
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@ footer .row.main {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer .row.main .branding > p {
 | 
			
		||||
  margin-bottom: 25px;
 | 
			
		||||
  margin-bottom: 45px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer .row.main .branding .btn {
 | 
			
		||||
 | 
			
		||||
@ -4,8 +4,10 @@
 | 
			
		||||
    "outDir": "./out-tsc/spec",
 | 
			
		||||
    "types": [
 | 
			
		||||
      "jasmine",
 | 
			
		||||
      "node"
 | 
			
		||||
    ]
 | 
			
		||||
      "node",
 | 
			
		||||
      "cypress",
 | 
			
		||||
      "cypress-wait-until"
 | 
			
		||||
]
 | 
			
		||||
  },
 | 
			
		||||
  "files": [
 | 
			
		||||
    "src/test.ts",
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
@reboot screen -dmS mainnet /bitcoin/electrs/electrs-start-mainnet
 | 
			
		||||
@reboot /usr/local/bin/bitcoind -testnet >/dev/null 2>&1
 | 
			
		||||
@reboot screen -dmS testnet /bitcoin/electrs/electrs-start-testnet
 | 
			
		||||
@reboot /usr/local/bin/bitcoind -signet >/dev/null 2>&1
 | 
			
		||||
@reboot screen -dmS signet /bitcoin/electrs/electrs-start-signet
 | 
			
		||||
@reboot sleep 5 ; /usr/local/bin/bitcoind -testnet >/dev/null 2>&1
 | 
			
		||||
@reboot sleep 5 ; /usr/local/bin/bitcoind -signet >/dev/null 2>&1
 | 
			
		||||
@reboot sleep 10 ; screen -dmS mainnet /bitcoin/electrs/electrs-start-mainnet
 | 
			
		||||
@reboot sleep 10 ; screen -dmS testnet /bitcoin/electrs/electrs-start-testnet
 | 
			
		||||
@reboot sleep 10 ; screen -dmS signet /bitcoin/electrs/electrs-start-signet
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,10 @@
 | 
			
		||||
# start elements on reboot
 | 
			
		||||
@reboot /usr/local/bin/elementsd -chain=liquidv1 >/dev/null 2>&1
 | 
			
		||||
@reboot /usr/local/bin/elementsd -chain=liquidtestnet >/dev/null 2>&1
 | 
			
		||||
@reboot sleep 5 ; /usr/local/bin/elementsd -chain=liquidv1 >/dev/null 2>&1
 | 
			
		||||
@reboot sleep 5 ; /usr/local/bin/elementsd -chain=liquidtestnet >/dev/null 2>&1
 | 
			
		||||
 | 
			
		||||
# start electrs on reboot
 | 
			
		||||
@reboot screen -dmS liquidv1 /elements/electrs/electrs-start-liquid
 | 
			
		||||
@reboot screen -dmS liquidtestnet /elements/electrs/electrs-start-liquidtestnet
 | 
			
		||||
@reboot sleep 20 ; screen -dmS liquidv1 /elements/electrs/electrs-start-liquid
 | 
			
		||||
@reboot sleep 20 ; screen -dmS liquidtestnet /elements/electrs/electrs-start-liquidtestnet
 | 
			
		||||
 | 
			
		||||
# hourly asset update and electrs restart
 | 
			
		||||
6 * * * * cd $HOME/asset_registry_db && git pull --quiet origin master && cd $HOME/asset_registry_testnet_db && git pull --quiet origin master && killall electrs
 | 
			
		||||
 | 
			
		||||
@ -1449,7 +1449,7 @@ if [ "${UNFURL_INSTALL}" = ON ];then
 | 
			
		||||
 | 
			
		||||
                echo "[*] Installing color emoji"
 | 
			
		||||
                osSudo "${ROOT_USER}" curl "https://github.com/samuelngs/apple-emoji-linux/releases/download/ios-15.4/AppleColorEmoji.ttf" -o /usr/local/share/fonts/TTF/AppleColorEmoji.ttf
 | 
			
		||||
                cat >> /usr/local/etc/fonts/conf.d/01-emoji.conf <<EOF
 | 
			
		||||
                cat > /usr/local/etc/fonts/conf.d/01-emoji.conf <<EOF
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
 | 
			
		||||
<fontconfig>
 | 
			
		||||
 | 
			
		||||
@ -23,8 +23,27 @@
 | 
			
		||||
    "PASSWORD": "__BITCOIN_RPC_PASS__"
 | 
			
		||||
  },
 | 
			
		||||
  "ESPLORA": {
 | 
			
		||||
    "REST_API_URL": "http://127.0.0.1:5001",
 | 
			
		||||
    "UNIX_SOCKET_PATH": "/elements/socket/esplora-liquid-mainnet"
 | 
			
		||||
    "UNIX_SOCKET_PATH": "/elements/socket/esplora-liquid-mainnet",
 | 
			
		||||
    "FALLBACK": [
 | 
			
		||||
      "http://node201.fmt.mempool.space:3001",
 | 
			
		||||
      "http://node202.fmt.mempool.space:3001",
 | 
			
		||||
      "http://node203.fmt.mempool.space:3001",
 | 
			
		||||
      "http://node204.fmt.mempool.space:3001",
 | 
			
		||||
      "http://node205.fmt.mempool.space:3001",
 | 
			
		||||
      "http://node206.fmt.mempool.space:3001",
 | 
			
		||||
      "http://node201.fra.mempool.space:3001",
 | 
			
		||||
      "http://node202.fra.mempool.space:3001",
 | 
			
		||||
      "http://node203.fra.mempool.space:3001",
 | 
			
		||||
      "http://node204.fra.mempool.space:3001",
 | 
			
		||||
      "http://node205.fra.mempool.space:3001",
 | 
			
		||||
      "http://node206.fra.mempool.space:3001",
 | 
			
		||||
      "http://node201.tk7.mempool.space:3001",
 | 
			
		||||
      "http://node202.tk7.mempool.space:3001",
 | 
			
		||||
      "http://node203.tk7.mempool.space:3001",
 | 
			
		||||
      "http://node204.tk7.mempool.space:3001",
 | 
			
		||||
      "http://node205.tk7.mempool.space:3001",
 | 
			
		||||
      "http://node206.tk7.mempool.space:3001"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "DATABASE": {
 | 
			
		||||
    "ENABLED": true,
 | 
			
		||||
 | 
			
		||||
@ -23,8 +23,27 @@
 | 
			
		||||
    "PASSWORD": "__BITCOIN_RPC_PASS__"
 | 
			
		||||
  },
 | 
			
		||||
  "ESPLORA": {
 | 
			
		||||
    "REST_API_URL": "http://127.0.0.1:5004",
 | 
			
		||||
    "UNIX_SOCKET_PATH": "/elements/socket/esplora-liquid-testnet"
 | 
			
		||||
    "UNIX_SOCKET_PATH": "/elements/socket/esplora-liquid-testnet",
 | 
			
		||||
    "FALLBACK": [
 | 
			
		||||
      "http://node201.fmt.mempool.space:3004",
 | 
			
		||||
      "http://node202.fmt.mempool.space:3004",
 | 
			
		||||
      "http://node203.fmt.mempool.space:3004",
 | 
			
		||||
      "http://node204.fmt.mempool.space:3004",
 | 
			
		||||
      "http://node205.fmt.mempool.space:3004",
 | 
			
		||||
      "http://node206.fmt.mempool.space:3004",
 | 
			
		||||
      "http://node201.fra.mempool.space:3004",
 | 
			
		||||
      "http://node202.fra.mempool.space:3004",
 | 
			
		||||
      "http://node203.fra.mempool.space:3004",
 | 
			
		||||
      "http://node204.fra.mempool.space:3004",
 | 
			
		||||
      "http://node205.fra.mempool.space:3004",
 | 
			
		||||
      "http://node206.fra.mempool.space:3004",
 | 
			
		||||
      "http://node201.tk7.mempool.space:3004",
 | 
			
		||||
      "http://node202.tk7.mempool.space:3004",
 | 
			
		||||
      "http://node203.tk7.mempool.space:3004",
 | 
			
		||||
      "http://node204.tk7.mempool.space:3004",
 | 
			
		||||
      "http://node205.tk7.mempool.space:3004",
 | 
			
		||||
      "http://node206.tk7.mempool.space:3004"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "DATABASE": {
 | 
			
		||||
    "ENABLED": true,
 | 
			
		||||
 | 
			
		||||
@ -35,8 +35,27 @@
 | 
			
		||||
    "PASSWORD": "__BITCOIN_RPC_PASS__"
 | 
			
		||||
  },
 | 
			
		||||
  "ESPLORA": {
 | 
			
		||||
    "REST_API_URL": "http://127.0.0.1:5000",
 | 
			
		||||
    "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-mainnet"
 | 
			
		||||
    "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-mainnet",
 | 
			
		||||
    "FALLBACK": [
 | 
			
		||||
      "http://node201.fmt.mempool.space:3000",
 | 
			
		||||
      "http://node202.fmt.mempool.space:3000",
 | 
			
		||||
      "http://node203.fmt.mempool.space:3000",
 | 
			
		||||
      "http://node204.fmt.mempool.space:3000",
 | 
			
		||||
      "http://node205.fmt.mempool.space:3000",
 | 
			
		||||
      "http://node206.fmt.mempool.space:3000",
 | 
			
		||||
      "http://node201.fra.mempool.space:3000",
 | 
			
		||||
      "http://node202.fra.mempool.space:3000",
 | 
			
		||||
      "http://node203.fra.mempool.space:3000",
 | 
			
		||||
      "http://node204.fra.mempool.space:3000",
 | 
			
		||||
      "http://node205.fra.mempool.space:3000",
 | 
			
		||||
      "http://node206.fra.mempool.space:3000",
 | 
			
		||||
      "http://node201.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node202.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node203.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node204.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node205.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node206.tk7.mempool.space:3000"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "DATABASE": {
 | 
			
		||||
    "ENABLED": true,
 | 
			
		||||
 | 
			
		||||
@ -25,8 +25,27 @@
 | 
			
		||||
    "PASSWORD": "__BITCOIN_RPC_PASS__"
 | 
			
		||||
  },
 | 
			
		||||
  "ESPLORA": {
 | 
			
		||||
    "REST_API_URL": "http://127.0.0.1:5003",
 | 
			
		||||
    "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-signet"
 | 
			
		||||
    "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-signet",
 | 
			
		||||
    "FALLBACK": [
 | 
			
		||||
      "http://node201.fmt.mempool.space:3003",
 | 
			
		||||
      "http://node202.fmt.mempool.space:3003",
 | 
			
		||||
      "http://node203.fmt.mempool.space:3003",
 | 
			
		||||
      "http://node204.fmt.mempool.space:3003",
 | 
			
		||||
      "http://node205.fmt.mempool.space:3003",
 | 
			
		||||
      "http://node206.fmt.mempool.space:3003",
 | 
			
		||||
      "http://node201.fra.mempool.space:3003",
 | 
			
		||||
      "http://node202.fra.mempool.space:3003",
 | 
			
		||||
      "http://node203.fra.mempool.space:3003",
 | 
			
		||||
      "http://node204.fra.mempool.space:3003",
 | 
			
		||||
      "http://node205.fra.mempool.space:3003",
 | 
			
		||||
      "http://node206.fra.mempool.space:3003",
 | 
			
		||||
      "http://node201.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node202.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node203.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node204.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node205.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node206.tk7.mempool.space:3003"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "DATABASE": {
 | 
			
		||||
    "ENABLED": true,
 | 
			
		||||
 | 
			
		||||
@ -25,8 +25,27 @@
 | 
			
		||||
    "PASSWORD": "__BITCOIN_RPC_PASS__"
 | 
			
		||||
  },
 | 
			
		||||
  "ESPLORA": {
 | 
			
		||||
    "REST_API_URL": "http://127.0.0.1:5002",
 | 
			
		||||
    "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-testnet"
 | 
			
		||||
    "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-testnet",
 | 
			
		||||
    "FALLBACK": [
 | 
			
		||||
      "http://node201.fmt.mempool.space:3002",
 | 
			
		||||
      "http://node202.fmt.mempool.space:3002",
 | 
			
		||||
      "http://node203.fmt.mempool.space:3002",
 | 
			
		||||
      "http://node204.fmt.mempool.space:3002",
 | 
			
		||||
      "http://node205.fmt.mempool.space:3002",
 | 
			
		||||
      "http://node206.fmt.mempool.space:3002",
 | 
			
		||||
      "http://node201.fra.mempool.space:3002",
 | 
			
		||||
      "http://node202.fra.mempool.space:3002",
 | 
			
		||||
      "http://node203.fra.mempool.space:3002",
 | 
			
		||||
      "http://node204.fra.mempool.space:3002",
 | 
			
		||||
      "http://node205.fra.mempool.space:3002",
 | 
			
		||||
      "http://node206.fra.mempool.space:3002",
 | 
			
		||||
      "http://node201.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node202.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node203.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node204.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node205.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node206.tk7.mempool.space:3002"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "DATABASE": {
 | 
			
		||||
    "ENABLED": true,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user