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 | version: 2 | ||||||
| updates: | updates: | ||||||
|   - package-ecosystem: npm |   - package-ecosystem: npm | ||||||
|  |     versioning-strategy: increase | ||||||
|     directory: "/backend" |     directory: "/backend" | ||||||
|     schedule: |     schedule: | ||||||
|       interval: daily |       interval: daily | ||||||
| @ -14,6 +15,7 @@ updates: | |||||||
| 
 | 
 | ||||||
|   - package-ecosystem: npm |   - package-ecosystem: npm | ||||||
|     directory: "/frontend" |     directory: "/frontend" | ||||||
|  |     versioning-strategy: increase | ||||||
|     schedule: |     schedule: | ||||||
|       interval: daily |       interval: daily | ||||||
|     open-pull-requests-limit: 10 |     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 |       - name: Unit Tests | ||||||
|         if: ${{ matrix.flavor == 'dev'}} |         if: ${{ matrix.flavor == 'dev'}} | ||||||
|         run: npm run test |         run: npm run test:ci | ||||||
|         working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend |         working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend | ||||||
| 
 | 
 | ||||||
|       - name: Build |       - name: Build | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								.github/workflows/cypress.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/cypress.yml
									
									
									
									
										vendored
									
									
								
							| @ -38,7 +38,7 @@ jobs: | |||||||
|       - name: Setup node |       - name: Setup node | ||||||
|         uses: actions/setup-node@v3 |         uses: actions/setup-node@v3 | ||||||
|         with: |         with: | ||||||
|           node-version: 16.15.0 |           node-version: 18 | ||||||
|           cache: "npm" |           cache: "npm" | ||||||
|           cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json |           cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -50,7 +50,8 @@ | |||||||
|   "ESPLORA": { |   "ESPLORA": { | ||||||
|     "REST_API_URL": "http://127.0.0.1:3000", |     "REST_API_URL": "http://127.0.0.1:3000", | ||||||
|     "UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet", |     "UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet", | ||||||
|     "RETRY_UNIX_SOCKET_AFTER": 30000 |     "RETRY_UNIX_SOCKET_AFTER": 30000, | ||||||
|  |     "FALLBACK": [] | ||||||
|   }, |   }, | ||||||
|   "SECOND_CORE_RPC": { |   "SECOND_CORE_RPC": { | ||||||
|     "HOST": "127.0.0.1", |     "HOST": "127.0.0.1", | ||||||
|  | |||||||
| @ -31,6 +31,7 @@ | |||||||
|     "reindex-updated-pools": "npm run start-production --update-pools", |     "reindex-updated-pools": "npm run start-production --update-pools", | ||||||
|     "reindex-all-blocks": "npm run start-production --update-pools --reindex-blocks", |     "reindex-all-blocks": "npm run start-production --update-pools --reindex-blocks", | ||||||
|     "test": "./node_modules/.bin/jest --coverage", |     "test": "./node_modules/.bin/jest --coverage", | ||||||
|  |     "test:ci": "CI=true ./node_modules/.bin/jest --coverage", | ||||||
|     "lint": "./node_modules/.bin/eslint . --ext .ts", |     "lint": "./node_modules/.bin/eslint . --ext .ts", | ||||||
|     "lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix", |     "lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix", | ||||||
|     "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"", |     "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"", | ||||||
|  | |||||||
| @ -51,7 +51,8 @@ | |||||||
|   "ESPLORA": { |   "ESPLORA": { | ||||||
|     "REST_API_URL": "__ESPLORA_REST_API_URL__", |     "REST_API_URL": "__ESPLORA_REST_API_URL__", | ||||||
|     "UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__", |     "UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__", | ||||||
|     "RETRY_UNIX_SOCKET_AFTER": 888 |     "RETRY_UNIX_SOCKET_AFTER": 888, | ||||||
|  |     "FALLBACK": [] | ||||||
|   }, |   }, | ||||||
|   "SECOND_CORE_RPC": { |   "SECOND_CORE_RPC": { | ||||||
|     "HOST": "__SECOND_CORE_RPC_HOST__", |     "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.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({ |       expect(config.CORE_RPC).toStrictEqual({ | ||||||
|         HOST: '127.0.0.1', |         HOST: '127.0.0.1', | ||||||
| @ -181,7 +186,9 @@ describe('Mempool Backend Config', () => { | |||||||
|         for (const [key, value] of Object.entries(jsonObj)) { |         for (const [key, value] of Object.entries(jsonObj)) { | ||||||
|           // We have a few cases where we can't follow the pattern
 |           // We have a few cases where we can't follow the pattern
 | ||||||
|           if (root === 'MEMPOOL' && key === 'HTTP_PORT') { |           if (root === 'MEMPOOL' && key === 'HTTP_PORT') { | ||||||
|  |             if (process.env.CI) { | ||||||
|               console.log('skipping check for MEMPOOL_HTTP_PORT'); |               console.log('skipping check for MEMPOOL_HTTP_PORT'); | ||||||
|  |             } | ||||||
|             continue; |             continue; | ||||||
|           } |           } | ||||||
|           switch (typeof value) { |           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:=(.*)}
 |               //The string used as the default value, to be checked as a regex, i.e, __MEMPOOL_ENABLED__=${MEMPOOL_ENABLED:=(.*)}
 | ||||||
|               const defaultEntry = replaceStr + '=' + '\\${' + envVarStr + ':=(.*)' + '}'; |               const defaultEntry = replaceStr + '=' + '\\${' + envVarStr + ':=(.*)' + '}'; | ||||||
| 
 | 
 | ||||||
|  |               if (process.env.CI) { | ||||||
|                 console.log(`looking for ${defaultEntry} in the start.sh script`); |                 console.log(`looking for ${defaultEntry} in the start.sh script`); | ||||||
|  |               } | ||||||
|               const re = new RegExp(defaultEntry); |               const re = new RegExp(defaultEntry); | ||||||
|               expect(startSh).toMatch(re); |               expect(startSh).toMatch(re); | ||||||
| 
 | 
 | ||||||
|               //The string that actually replaces the values in the config file
 |               //The string that actually replaces the values in the config file
 | ||||||
|               const sedStr = 'sed -i "s!' + replaceStr + '!${' + replaceStr + '}!g" mempool-config.json'; |               const sedStr = 'sed -i "s!' + replaceStr + '!${' + replaceStr + '}!g" mempool-config.json'; | ||||||
|  |               if (process.env.CI) { | ||||||
|                 console.log(`looking for ${sedStr} in the start.sh script`); |                 console.log(`looking for ${sedStr} in the start.sh script`); | ||||||
|  |               } | ||||||
|               expect(startSh).toContain(sedStr); |               expect(startSh).toContain(sedStr); | ||||||
|               break; |               break; | ||||||
|             } |             } | ||||||
|  | |||||||
| @ -23,6 +23,8 @@ export interface AbstractBitcoinApi { | |||||||
|   $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>; |   $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>; | ||||||
|   $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>; |   $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>; | ||||||
|   $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>; |   $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>; | ||||||
|  | 
 | ||||||
|  |   startHealthChecks(): void; | ||||||
| } | } | ||||||
| export interface BitcoinRpcCredentials { | export interface BitcoinRpcCredentials { | ||||||
|   host: string; |   host: string; | ||||||
|  | |||||||
| @ -355,6 +355,7 @@ class BitcoinApi implements AbstractBitcoinApi { | |||||||
|     return transaction; |     return transaction; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   public startHealthChecks(): void {}; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default BitcoinApi; | export default BitcoinApi; | ||||||
|  | |||||||
| @ -1,135 +1,260 @@ | |||||||
| import config from '../../config'; | import config from '../../config'; | ||||||
| import axios, { AxiosRequestConfig } from 'axios'; | import axios, { AxiosResponse } from 'axios'; | ||||||
| import http from 'http'; | import http from 'http'; | ||||||
| import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; | import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; | ||||||
| import { IEsploraApi } from './esplora-api.interface'; | import { IEsploraApi } from './esplora-api.interface'; | ||||||
| import logger from '../../logger'; | import logger from '../../logger'; | ||||||
| 
 | 
 | ||||||
| const axiosConnection = axios.create({ | interface FailoverHost { | ||||||
|   httpAgent: new http.Agent({ keepAlive: true, }) |   host: string, | ||||||
|  |   rtts: number[], | ||||||
|  |   rtt: number | ||||||
|  |   failures: number, | ||||||
|  |   socket?: boolean, | ||||||
|  |   outOfSync?: boolean, | ||||||
|  |   unreachable?: boolean, | ||||||
|  |   preferred?: boolean, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 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 }) | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
| 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; |  | ||||||
| 
 |  | ||||||
|   constructor() { |   constructor() { | ||||||
|     this.activeAxiosConfig = this.axiosConfigWithUnixSocket; |     // setup list of hosts
 | ||||||
|   } |     this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => { | ||||||
| 
 |       return { | ||||||
|   fallbackToTcpSocket() { |         host: domain, | ||||||
|     if (!this.unixSocketRetryTimeout) { |         rtts: [], | ||||||
|       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`); |         rtt: Infinity, | ||||||
|       // Retry the unix socket after a few seconds
 |         failures: 0, | ||||||
|       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); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Use the TCP socket (reach a different esplora instance through nginx)
 |  | ||||||
|     this.activeAxiosConfig = this.axiosConfigTcpSocketOnly; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   $queryWrapper<T>(url, responseType = 'json'): Promise<T> { |  | ||||||
|     return axiosConnection.get<T>(url, { ...this.activeAxiosConfig, responseType: responseType }) |  | ||||||
|       .then((response) => response.data) |  | ||||||
|       .catch((e) => { |  | ||||||
|         if (e?.code === 'ECONNREFUSED') { |  | ||||||
|           this.fallbackToTcpSocket(); |  | ||||||
|           // 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; |  | ||||||
|     }); |     }); | ||||||
|  |     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; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   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); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     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); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // 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) => { | ||||||
|  |         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 this.$query(method, path, data, responseType, fallbackHost, false); | ||||||
|         } else { |         } else { | ||||||
|           throw e; |           throw e; | ||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   $postWrapper<T>(url, body, responseType = 'json', params: any = undefined): Promise<T> { |   public async $get<T>(path, responseType = 'json'): Promise<T> { | ||||||
|     return axiosConnection.post<T>(url, body, { ...this.activeAxiosConfig, responseType: responseType, params }) |     return this.$query<T>('get', path, null, responseType); | ||||||
|       .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 $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'][]> { |   $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> { |   $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[]> { |   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[]> { |   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> { |   $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> { |   $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> { |   $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[]> { |   $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[]> { |   $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> { |   $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> { |   $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> { |   $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> { |   $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); }); |       .then((response) => { return Buffer.from(response.data); }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -158,11 +283,11 @@ class ElectrsApi implements AbstractBitcoinApi { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> { |   $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[]> { |   $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[][]> { |   async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> { | ||||||
| @ -173,6 +298,10 @@ class ElectrsApi implements AbstractBitcoinApi { | |||||||
|     } |     } | ||||||
|     return outspends; |     return outspends; | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   public startHealthChecks(): void { | ||||||
|  |     this.failoverRouter.startHealthChecks(); | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default ElectrsApi; | export default ElectrsApi; | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ import PricesRepository from '../../repositories/PricesRepository'; | |||||||
| class MiningRoutes { | class MiningRoutes { | ||||||
|   public initRoutes(app: Application) { |   public initRoutes(app: Application) { | ||||||
|     app |     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/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/hashrate', this.$getPoolHistoricalHashrate) | ||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks', this.$getPoolBlocks) |       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks', this.$getPoolBlocks) | ||||||
| @ -41,6 +42,10 @@ class MiningRoutes { | |||||||
|       res.header('Pragma', 'public'); |       res.header('Pragma', 'public'); | ||||||
|       res.header('Cache-control', 'public'); |       res.header('Cache-control', 'public'); | ||||||
|       res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); |       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) { |       if (req.query.timestamp) { | ||||||
|         res.status(200).send(await PricesRepository.$getNearestHistoricalPrice( |         res.status(200).send(await PricesRepository.$getNearestHistoricalPrice( | ||||||
|           parseInt(<string>req.query.timestamp ?? 0, 10) |           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) { |   private async $getPools(req: Request, res: Response) { | ||||||
|     try { |     try { | ||||||
|       const stats = await mining.$getPoolsStats(req.params.interval); |       const stats = await mining.$getPoolsStats(req.params.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 { |   private getDateMidnight(date: Date): Date { | ||||||
|     date.setUTCHours(0); |     date.setUTCHours(0); | ||||||
|     date.setUTCMinutes(0); |     date.setUTCMinutes(0); | ||||||
|  | |||||||
| @ -198,18 +198,14 @@ class WebsocketHandler { | |||||||
|                 matchedAddress = matchedAddress.toLowerCase(); |                 matchedAddress = matchedAddress.toLowerCase(); | ||||||
|               } |               } | ||||||
|               if (/^04[a-fA-F0-9]{128}$/.test(parsedMessage['track-address'])) { |               if (/^04[a-fA-F0-9]{128}$/.test(parsedMessage['track-address'])) { | ||||||
|                 client['track-address'] = null; |                 client['track-address'] = '41' + matchedAddress + 'ac'; | ||||||
|                 client['track-scriptpubkey'] = '41' + matchedAddress + 'ac'; |               } else if (/^(02|03)[a-fA-F0-9]{64}$/.test(parsedMessage['track-address'])) { | ||||||
|               } else if (/^|(02|03)[a-fA-F0-9]{64}$/.test(parsedMessage['track-address'])) { |                 client['track-address'] = '21' + matchedAddress + 'ac'; | ||||||
|                 client['track-address'] = null; |  | ||||||
|                 client['track-scriptpubkey'] = '21' + matchedAddress + 'ac'; |  | ||||||
|               } else { |               } else { | ||||||
|                 client['track-address'] = matchedAddress; |                 client['track-address'] = matchedAddress; | ||||||
|                 client['track-scriptpubkey'] = null; |  | ||||||
|               } |               } | ||||||
|             } else { |             } else { | ||||||
|               client['track-address'] = null; |               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) => { |     this.wss.clients.forEach(async (client) => { | ||||||
|       if (client.readyState !== WebSocket.OPEN) { |       if (client.readyState !== WebSocket.OPEN) { | ||||||
|         return; |         return; | ||||||
| @ -527,78 +526,13 @@ class WebsocketHandler { | |||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       if (client['track-address']) { |       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) { |         if (fullTransactions.length) { | ||||||
|           const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_address === client['track-address']); |           response['address-transactions'] = JSON.stringify(fullTransactions); | ||||||
|           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); |  | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
| @ -606,7 +540,6 @@ class WebsocketHandler { | |||||||
|         const foundTransactions: TransactionExtended[] = []; |         const foundTransactions: TransactionExtended[] = []; | ||||||
| 
 | 
 | ||||||
|         newTransactions.forEach((tx) => { |         newTransactions.forEach((tx) => { | ||||||
| 
 |  | ||||||
|           if (client['track-asset'] === Common.nativeAssetId) { |           if (client['track-asset'] === Common.nativeAssetId) { | ||||||
|             if (tx.vin.some((vin) => !!vin.is_pegin)) { |             if (tx.vin.some((vin) => !!vin.is_pegin)) { | ||||||
|               foundTransactions.push(tx); |               foundTransactions.push(tx); | ||||||
| @ -805,6 +738,9 @@ class WebsocketHandler { | |||||||
|     const fees = feeApi.getRecommendedFee(); |     const fees = feeApi.getRecommendedFee(); | ||||||
|     const mempoolInfo = memPool.getMempoolInfo(); |     const mempoolInfo = memPool.getMempoolInfo(); | ||||||
| 
 | 
 | ||||||
|  |     // pre-compute address transactions
 | ||||||
|  |     const addressCache = this.makeAddressCache(transactions); | ||||||
|  | 
 | ||||||
|     // update init data
 |     // update init data
 | ||||||
|     this.updateSocketDataFields({ |     this.updateSocketDataFields({ | ||||||
|       'mempoolInfo': mempoolInfo, |       'mempoolInfo': mempoolInfo, | ||||||
| @ -867,44 +803,7 @@ class WebsocketHandler { | |||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       if (client['track-address']) { |       if (client['track-address']) { | ||||||
|         const foundTransactions: TransactionExtended[] = []; |         const foundTransactions: TransactionExtended[] = Array.from(addressCache[client['track-address']]?.values() || []); | ||||||
| 
 |  | ||||||
|         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); |  | ||||||
|           } |  | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         if (foundTransactions.length) { |         if (foundTransactions.length) { | ||||||
|           foundTransactions.forEach((tx) => { |           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 { |   private printLogs(): void { | ||||||
|     if (this.wss) { |     if (this.wss) { | ||||||
|       const count = this.wss?.clients?.size || 0; |       const count = this.wss?.clients?.size || 0; | ||||||
|  | |||||||
| @ -44,6 +44,7 @@ interface IConfig { | |||||||
|     REST_API_URL: string; |     REST_API_URL: string; | ||||||
|     UNIX_SOCKET_PATH: string | void | null; |     UNIX_SOCKET_PATH: string | void | null; | ||||||
|     RETRY_UNIX_SOCKET_AFTER: number; |     RETRY_UNIX_SOCKET_AFTER: number; | ||||||
|  |     FALLBACK: string[]; | ||||||
|   }; |   }; | ||||||
|   LIGHTNING: { |   LIGHTNING: { | ||||||
|     ENABLED: boolean; |     ENABLED: boolean; | ||||||
| @ -188,6 +189,7 @@ const defaults: IConfig = { | |||||||
|     'REST_API_URL': 'http://127.0.0.1:3000', |     'REST_API_URL': 'http://127.0.0.1:3000', | ||||||
|     'UNIX_SOCKET_PATH': null, |     'UNIX_SOCKET_PATH': null, | ||||||
|     'RETRY_UNIX_SOCKET_AFTER': 30000, |     'RETRY_UNIX_SOCKET_AFTER': 30000, | ||||||
|  |     'FALLBACK': [], | ||||||
|   }, |   }, | ||||||
|   'ELECTRUM': { |   'ELECTRUM': { | ||||||
|     'HOST': '127.0.0.1', |     'HOST': '127.0.0.1', | ||||||
|  | |||||||
| @ -91,6 +91,10 @@ class Server { | |||||||
|   async startServer(worker = false): Promise<void> { |   async startServer(worker = false): Promise<void> { | ||||||
|     logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`); |     logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`); | ||||||
| 
 | 
 | ||||||
|  |     if (config.MEMPOOL.BACKEND === 'esplora') { | ||||||
|  |       bitcoinApi.startHealthChecks(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     if (config.DATABASE.ENABLED) { |     if (config.DATABASE.ENABLED) { | ||||||
|       await DB.checkDbConnection(); |       await DB.checkDbConnection(); | ||||||
|       try { |       try { | ||||||
|  | |||||||
| @ -51,7 +51,8 @@ | |||||||
|   "ESPLORA": { |   "ESPLORA": { | ||||||
|     "REST_API_URL": "__ESPLORA_REST_API_URL__", |     "REST_API_URL": "__ESPLORA_REST_API_URL__", | ||||||
|     "UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__", |     "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": { |   "SECOND_CORE_RPC": { | ||||||
|     "HOST": "__SECOND_CORE_RPC_HOST__", |     "HOST": "__SECOND_CORE_RPC_HOST__", | ||||||
|  | |||||||
| @ -42,9 +42,6 @@ | |||||||
| // -- This will overwrite an existing command --
 | // -- This will overwrite an existing command --
 | ||||||
| // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
 | ||||||
| 
 | 
 | ||||||
| 'use strict' |  | ||||||
| 
 |  | ||||||
| import 'cypress-wait-until'; |  | ||||||
| import { PageIdleDetector } from './PageIdleDetector'; | import { PageIdleDetector } from './PageIdleDetector'; | ||||||
| import { mockWebSocket } from './websocket'; | import { mockWebSocket } from './websocket'; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -14,6 +14,7 @@ | |||||||
| // ***********************************************************
 | // ***********************************************************
 | ||||||
| 
 | 
 | ||||||
| // When a command from ./commands is ready to use, import with `import './commands'` syntax
 | // When a command from ./commands is ready to use, import with `import './commands'` syntax
 | ||||||
|  | import 'cypress-wait-until'; | ||||||
| import './commands'; | import './commands'; | ||||||
| import failOnConsoleError from 'cypress-fail-on-console-error'; | import failOnConsoleError from 'cypress-fail-on-console-error'; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
|   "extends": "../tsconfig.json", |   "extends": "../tsconfig.json", | ||||||
|   "include": ["**/*.ts"], |   "include": ["**/*.ts"], | ||||||
|   "compilerOptions": { |   "compilerOptions": { | ||||||
|     "types": ["cypress"], |     "types": ["cypress", "node", "cypress-wait-until"], | ||||||
|     "lib": ["es2015", "dom"], |     "lib": ["es2015", "dom"], | ||||||
|     "allowJs": true, |     "allowJs": true, | ||||||
|     "noEmit": true, |     "noEmit": true, | ||||||
|  | |||||||
							
								
								
									
										54
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										54
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -58,9 +58,10 @@ | |||||||
|       }, |       }, | ||||||
|       "optionalDependencies": { |       "optionalDependencies": { | ||||||
|         "@cypress/schematic": "^2.5.0", |         "@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-fail-on-console-error": "~4.0.3", | ||||||
|         "cypress-wait-until": "^1.7.2", |         "cypress-wait-until": "^2.0.0", | ||||||
|         "mock-socket": "~9.2.1", |         "mock-socket": "~9.2.1", | ||||||
|         "start-server-and-test": "~2.0.0" |         "start-server-and-test": "~2.0.0" | ||||||
|       } |       } | ||||||
| @ -3925,6 +3926,16 @@ | |||||||
|         "@types/node": "*" |         "@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": { |     "node_modules/@types/eslint": { | ||||||
|       "version": "8.4.1", |       "version": "8.4.1", | ||||||
|       "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", |       "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", | ||||||
| @ -6641,9 +6652,9 @@ | |||||||
|       "peer": true |       "peer": true | ||||||
|     }, |     }, | ||||||
|     "node_modules/cypress": { |     "node_modules/cypress": { | ||||||
|       "version": "12.17.1", |       "version": "12.17.2", | ||||||
|       "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.17.1.tgz", |       "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.17.2.tgz", | ||||||
|       "integrity": "sha512-eKfBgO6t8waEyhegL4gxD7tcI6uTCGttu+ZU7y9Hq8BlpMztd7iLeIF4AJFAnbZH1xjX+wwgg4cRKFNSvv3VWQ==", |       "integrity": "sha512-hxWAaWbqQBzzMuadSGSuQg5PDvIGOovm6xm0hIfpCVcORsCAj/gF2p0EvfnJ4f+jK2PCiDgP6D2eeE9/FK4Mjg==", | ||||||
|       "hasInstallScript": true, |       "hasInstallScript": true, | ||||||
|       "optional": true, |       "optional": true, | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
| @ -6710,10 +6721,14 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/cypress-wait-until": { |     "node_modules/cypress-wait-until": { | ||||||
|       "version": "1.7.2", |       "version": "2.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/cypress-wait-until/-/cypress-wait-until-1.7.2.tgz", |       "resolved": "https://registry.npmjs.org/cypress-wait-until/-/cypress-wait-until-2.0.0.tgz", | ||||||
|       "integrity": "sha512-uZ+M8/MqRcpf+FII/UZrU7g1qYZ4aVlHcgyVopnladyoBrpoaMJ4PKZDrdOJ05H5RHbr7s9Tid635X3E+ZLU/Q==", |       "integrity": "sha512-ulUZyrWBn+OuC8oiQuGKAScDYfpaWnE3dEE/raUo64w4RHQxZrQ/iMIWT4ZjGMMPr3P+BFEALCRnjQeRqzZj6g==", | ||||||
|       "optional": true |       "optional": true, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=18.16.0", | ||||||
|  |         "npm": ">=9.5.1" | ||||||
|  |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/cypress/node_modules/@types/node": { |     "node_modules/cypress/node_modules/@types/node": { | ||||||
|       "version": "14.18.53", |       "version": "14.18.53", | ||||||
| @ -18862,6 +18877,15 @@ | |||||||
|         "@types/node": "*" |         "@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": { |     "@types/eslint": { | ||||||
|       "version": "8.4.1", |       "version": "8.4.1", | ||||||
|       "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", |       "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", | ||||||
| @ -20968,9 +20992,9 @@ | |||||||
|       "peer": true |       "peer": true | ||||||
|     }, |     }, | ||||||
|     "cypress": { |     "cypress": { | ||||||
|       "version": "12.17.1", |       "version": "12.17.2", | ||||||
|       "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.17.1.tgz", |       "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.17.2.tgz", | ||||||
|       "integrity": "sha512-eKfBgO6t8waEyhegL4gxD7tcI6uTCGttu+ZU7y9Hq8BlpMztd7iLeIF4AJFAnbZH1xjX+wwgg4cRKFNSvv3VWQ==", |       "integrity": "sha512-hxWAaWbqQBzzMuadSGSuQg5PDvIGOovm6xm0hIfpCVcORsCAj/gF2p0EvfnJ4f+jK2PCiDgP6D2eeE9/FK4Mjg==", | ||||||
|       "optional": true, |       "optional": true, | ||||||
|       "requires": { |       "requires": { | ||||||
|         "@cypress/request": "^2.88.11", |         "@cypress/request": "^2.88.11", | ||||||
| @ -21151,9 +21175,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "cypress-wait-until": { |     "cypress-wait-until": { | ||||||
|       "version": "1.7.2", |       "version": "2.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/cypress-wait-until/-/cypress-wait-until-1.7.2.tgz", |       "resolved": "https://registry.npmjs.org/cypress-wait-until/-/cypress-wait-until-2.0.0.tgz", | ||||||
|       "integrity": "sha512-uZ+M8/MqRcpf+FII/UZrU7g1qYZ4aVlHcgyVopnladyoBrpoaMJ4PKZDrdOJ05H5RHbr7s9Tid635X3E+ZLU/Q==", |       "integrity": "sha512-ulUZyrWBn+OuC8oiQuGKAScDYfpaWnE3dEE/raUo64w4RHQxZrQ/iMIWT4ZjGMMPr3P+BFEALCRnjQeRqzZj6g==", | ||||||
|       "optional": true |       "optional": true | ||||||
|     }, |     }, | ||||||
|     "d": { |     "d": { | ||||||
|  | |||||||
| @ -110,9 +110,10 @@ | |||||||
|   }, |   }, | ||||||
|   "optionalDependencies": { |   "optionalDependencies": { | ||||||
|     "@cypress/schematic": "^2.5.0", |     "@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-fail-on-console-error": "~4.0.3", | ||||||
|     "cypress-wait-until": "^1.7.2", |     "cypress-wait-until": "^2.0.0", | ||||||
|     "mock-socket": "~9.2.1", |     "mock-socket": "~9.2.1", | ||||||
|     "start-server-and-test": "~2.0.0" |     "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 { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; | ||||||
| import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, | import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, | ||||||
|   PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit } from '../interfaces/node-api.interface'; |   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 { StateService } from './state.service'; | ||||||
| import { WebsocketResponse } from '../interfaces/websocket.interface'; | import { WebsocketResponse } from '../interfaces/websocket.interface'; | ||||||
| import { Outspend, Transaction } from '../interfaces/electrs.interface'; | import { Outspend, Transaction } from '../interfaces/electrs.interface'; | ||||||
| @ -312,6 +312,19 @@ export class ApiService { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   getHistoricalPrice$(timestamp: number | undefined): Observable<Conversion> { |   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>( |     return this.httpClient.get<Conversion>( | ||||||
|       this.apiBaseUrl + this.apiBasePath + '/api/v1/historical-price' + |       this.apiBaseUrl + this.apiBasePath + '/api/v1/historical-price' + | ||||||
|         (timestamp ? `?timestamp=${timestamp}` : '') |         (timestamp ? `?timestamp=${timestamp}` : '') | ||||||
|  | |||||||
| @ -339,6 +339,10 @@ export class StateService { | |||||||
|     return this.network === 'liquid' || this.network === 'liquidtestnet'; |     return this.network === 'liquid' || this.network === 'liquidtestnet'; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   isAnyTestnet(): boolean { | ||||||
|  |     return ['testnet', 'signet', 'liquidtestnet'].includes(this.network); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   resetChainTip() { |   resetChainTip() { | ||||||
|     this.latestBlockHeight = -1; |     this.latestBlockHeight = -1; | ||||||
|     this.chainTip$.next(-1); |     this.chainTip$.next(-1); | ||||||
|  | |||||||
| @ -18,7 +18,7 @@ footer .row.main { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| footer .row.main .branding > p { | footer .row.main .branding > p { | ||||||
|   margin-bottom: 25px; |   margin-bottom: 45px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| footer .row.main .branding .btn { | footer .row.main .branding .btn { | ||||||
|  | |||||||
| @ -4,7 +4,9 @@ | |||||||
|     "outDir": "./out-tsc/spec", |     "outDir": "./out-tsc/spec", | ||||||
|     "types": [ |     "types": [ | ||||||
|       "jasmine", |       "jasmine", | ||||||
|       "node" |       "node", | ||||||
|  |       "cypress", | ||||||
|  |       "cypress-wait-until" | ||||||
| ] | ] | ||||||
|   }, |   }, | ||||||
|   "files": [ |   "files": [ | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| @reboot screen -dmS mainnet /bitcoin/electrs/electrs-start-mainnet | @reboot sleep 5 ; /usr/local/bin/bitcoind -testnet >/dev/null 2>&1 | ||||||
| @reboot /usr/local/bin/bitcoind -testnet >/dev/null 2>&1 | @reboot sleep 5 ; /usr/local/bin/bitcoind -signet >/dev/null 2>&1 | ||||||
| @reboot screen -dmS testnet /bitcoin/electrs/electrs-start-testnet | @reboot sleep 10 ; screen -dmS mainnet /bitcoin/electrs/electrs-start-mainnet | ||||||
| @reboot /usr/local/bin/bitcoind -signet >/dev/null 2>&1 | @reboot sleep 10 ; screen -dmS testnet /bitcoin/electrs/electrs-start-testnet | ||||||
| @reboot screen -dmS signet /bitcoin/electrs/electrs-start-signet | @reboot sleep 10 ; screen -dmS signet /bitcoin/electrs/electrs-start-signet | ||||||
|  | |||||||
| @ -1,10 +1,10 @@ | |||||||
| # start elements on reboot | # start elements on reboot | ||||||
| @reboot /usr/local/bin/elementsd -chain=liquidv1 >/dev/null 2>&1 | @reboot sleep 5 ; /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=liquidtestnet >/dev/null 2>&1 | ||||||
| 
 | 
 | ||||||
| # start electrs on reboot | # start electrs on reboot | ||||||
| @reboot screen -dmS liquidv1 /elements/electrs/electrs-start-liquid | @reboot sleep 20 ; screen -dmS liquidv1 /elements/electrs/electrs-start-liquid | ||||||
| @reboot screen -dmS liquidtestnet /elements/electrs/electrs-start-liquidtestnet | @reboot sleep 20 ; screen -dmS liquidtestnet /elements/electrs/electrs-start-liquidtestnet | ||||||
| 
 | 
 | ||||||
| # hourly asset update and electrs restart | # 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 | 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" |                 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 |                 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"?> | <?xml version="1.0" encoding="UTF-8"?> | ||||||
| <!DOCTYPE fontconfig SYSTEM "fonts.dtd"> | <!DOCTYPE fontconfig SYSTEM "fonts.dtd"> | ||||||
| <fontconfig> | <fontconfig> | ||||||
|  | |||||||
| @ -23,8 +23,27 @@ | |||||||
|     "PASSWORD": "__BITCOIN_RPC_PASS__" |     "PASSWORD": "__BITCOIN_RPC_PASS__" | ||||||
|   }, |   }, | ||||||
|   "ESPLORA": { |   "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": { |   "DATABASE": { | ||||||
|     "ENABLED": true, |     "ENABLED": true, | ||||||
|  | |||||||
| @ -23,8 +23,27 @@ | |||||||
|     "PASSWORD": "__BITCOIN_RPC_PASS__" |     "PASSWORD": "__BITCOIN_RPC_PASS__" | ||||||
|   }, |   }, | ||||||
|   "ESPLORA": { |   "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": { |   "DATABASE": { | ||||||
|     "ENABLED": true, |     "ENABLED": true, | ||||||
|  | |||||||
| @ -35,8 +35,27 @@ | |||||||
|     "PASSWORD": "__BITCOIN_RPC_PASS__" |     "PASSWORD": "__BITCOIN_RPC_PASS__" | ||||||
|   }, |   }, | ||||||
|   "ESPLORA": { |   "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": { |   "DATABASE": { | ||||||
|     "ENABLED": true, |     "ENABLED": true, | ||||||
|  | |||||||
| @ -25,8 +25,27 @@ | |||||||
|     "PASSWORD": "__BITCOIN_RPC_PASS__" |     "PASSWORD": "__BITCOIN_RPC_PASS__" | ||||||
|   }, |   }, | ||||||
|   "ESPLORA": { |   "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": { |   "DATABASE": { | ||||||
|     "ENABLED": true, |     "ENABLED": true, | ||||||
|  | |||||||
| @ -25,8 +25,27 @@ | |||||||
|     "PASSWORD": "__BITCOIN_RPC_PASS__" |     "PASSWORD": "__BITCOIN_RPC_PASS__" | ||||||
|   }, |   }, | ||||||
|   "ESPLORA": { |   "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": { |   "DATABASE": { | ||||||
|     "ENABLED": true, |     "ENABLED": true, | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user