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') { | ||||
|             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 + ':=(.*)' + '}'; | ||||
| 
 | ||||
|               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'; | ||||
|               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 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() { | ||||
|     this.activeAxiosConfig = this.axiosConfigWithUnixSocket; | ||||
|   } | ||||
| 
 | ||||
|   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); | ||||
|     } | ||||
| 
 | ||||
|     // 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; | ||||
|     // 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; | ||||
|   } | ||||
| 
 | ||||
|   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 { | ||||
|           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); | ||||
|  | ||||
| @ -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,7 +4,9 @@ | ||||
|     "outDir": "./out-tsc/spec", | ||||
|     "types": [ | ||||
|       "jasmine", | ||||
|       "node" | ||||
|       "node", | ||||
|       "cypress", | ||||
|       "cypress-wait-until" | ||||
| ] | ||||
|   }, | ||||
|   "files": [ | ||||
|  | ||||
| @ -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