Merge branch 'master' into knorrium/update-node-matrix
This commit is contained in:
		
						commit
						5f5e96984a
					
				
							
								
								
									
										72
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										72
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@ -251,17 +251,7 @@ jobs:
 | 
				
			|||||||
    strategy:
 | 
					    strategy:
 | 
				
			||||||
      fail-fast: false
 | 
					      fail-fast: false
 | 
				
			||||||
      matrix:
 | 
					      matrix:
 | 
				
			||||||
        module: ["mempool", "liquid"]
 | 
					        module: ["mempool", "liquid", "testnet4"]
 | 
				
			||||||
        include:
 | 
					 | 
				
			||||||
          - module: "mempool"
 | 
					 | 
				
			||||||
            spec: |
 | 
					 | 
				
			||||||
              cypress/e2e/mainnet/*.spec.ts
 | 
					 | 
				
			||||||
              cypress/e2e/signet/*.spec.ts
 | 
					 | 
				
			||||||
              cypress/e2e/testnet4/*.spec.ts
 | 
					 | 
				
			||||||
          - module: "liquid"
 | 
					 | 
				
			||||||
            spec: |
 | 
					 | 
				
			||||||
              cypress/e2e/liquid/liquid.spec.ts
 | 
					 | 
				
			||||||
              cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    name: E2E tests for ${{ matrix.module }}
 | 
					    name: E2E tests for ${{ matrix.module }}
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
@ -310,8 +300,10 @@ jobs:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      - name: Unzip assets before building (src/resources)
 | 
					      - name: Unzip assets before building (src/resources)
 | 
				
			||||||
        run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video
 | 
					        run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
 | 
					      # mempool
 | 
				
			||||||
      - name: Chrome browser tests (${{ matrix.module }})
 | 
					      - name: Chrome browser tests (${{ matrix.module }})
 | 
				
			||||||
 | 
					        if: ${{ matrix.module == 'mempool' }}
 | 
				
			||||||
        uses: cypress-io/github-action@v5
 | 
					        uses: cypress-io/github-action@v5
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          tag: ${{ github.event_name }}
 | 
					          tag: ${{ github.event_name }}
 | 
				
			||||||
@ -322,7 +314,9 @@ jobs:
 | 
				
			|||||||
          wait-on-timeout: 120
 | 
					          wait-on-timeout: 120
 | 
				
			||||||
          record: true
 | 
					          record: true
 | 
				
			||||||
          parallel: true
 | 
					          parallel: true
 | 
				
			||||||
          spec: ${{ matrix.spec }}
 | 
					          spec: |
 | 
				
			||||||
 | 
					            cypress/e2e/mainnet/*.spec.ts
 | 
				
			||||||
 | 
					            cypress/e2e/signet/*.spec.ts
 | 
				
			||||||
          group: Tests on Chrome (${{ matrix.module }})
 | 
					          group: Tests on Chrome (${{ matrix.module }})
 | 
				
			||||||
          browser: "chrome"
 | 
					          browser: "chrome"
 | 
				
			||||||
          ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
 | 
					          ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
 | 
				
			||||||
@ -332,6 +326,56 @@ jobs:
 | 
				
			|||||||
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 | 
					          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 | 
				
			||||||
          CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
 | 
					          CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      # liquid
 | 
				
			||||||
 | 
					      - name: Chrome browser tests (${{ matrix.module }})
 | 
				
			||||||
 | 
					        if: ${{ matrix.module == 'liquid' }}
 | 
				
			||||||
 | 
					        uses: cypress-io/github-action@v5
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          tag: ${{ github.event_name }}
 | 
				
			||||||
 | 
					          working-directory: ${{ matrix.module }}/frontend
 | 
				
			||||||
 | 
					          build: npm run config:defaults:${{ matrix.module }}
 | 
				
			||||||
 | 
					          start: npm run start:local-staging
 | 
				
			||||||
 | 
					          wait-on: "http://localhost:4200"
 | 
				
			||||||
 | 
					          wait-on-timeout: 120
 | 
				
			||||||
 | 
					          record: true
 | 
				
			||||||
 | 
					          parallel: true
 | 
				
			||||||
 | 
					          spec: |
 | 
				
			||||||
 | 
					            cypress/e2e/liquid/liquid.spec.ts
 | 
				
			||||||
 | 
					            cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
 | 
				
			||||||
 | 
					          group: Tests on Chrome (${{ matrix.module }})
 | 
				
			||||||
 | 
					          browser: "chrome"
 | 
				
			||||||
 | 
					          ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
 | 
				
			||||||
 | 
					          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
 | 
				
			||||||
 | 
					          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 | 
				
			||||||
 | 
					          CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      # testnet
 | 
				
			||||||
 | 
					      - name: Chrome browser tests (${{ matrix.module }})
 | 
				
			||||||
 | 
					        if: ${{ matrix.module == 'testnet4' }}
 | 
				
			||||||
 | 
					        uses: cypress-io/github-action@v5
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          tag: ${{ github.event_name }}
 | 
				
			||||||
 | 
					          working-directory: ${{ matrix.module }}/frontend
 | 
				
			||||||
 | 
					          build: npm run config:defaults:mempool
 | 
				
			||||||
 | 
					          start: npm run start:local-staging
 | 
				
			||||||
 | 
					          wait-on: "http://localhost:4200"
 | 
				
			||||||
 | 
					          wait-on-timeout: 120
 | 
				
			||||||
 | 
					          record: true
 | 
				
			||||||
 | 
					          parallel: true
 | 
				
			||||||
 | 
					          spec: |
 | 
				
			||||||
 | 
					            cypress/e2e/testnet4/*.spec.ts
 | 
				
			||||||
 | 
					          group: Tests on Chrome (${{ matrix.module }})
 | 
				
			||||||
 | 
					          browser: "chrome"
 | 
				
			||||||
 | 
					          ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          CYPRESS_REROUTE_TESTNET: true
 | 
				
			||||||
 | 
					          COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
 | 
				
			||||||
 | 
					          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
 | 
				
			||||||
 | 
					          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 | 
				
			||||||
 | 
					          CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  validate_docker_json:
 | 
					  validate_docker_json:
 | 
				
			||||||
    if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
 | 
					    if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
 | 
				
			||||||
    runs-on: "ubuntu-latest"
 | 
					    runs-on: "ubuntu-latest"
 | 
				
			||||||
@ -359,4 +403,4 @@ jobs:
 | 
				
			|||||||
      - name: Validate JSON syntax
 | 
					      - name: Validate JSON syntax
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          cat mempool-config.json | jq
 | 
					          cat mempool-config.json | jq
 | 
				
			||||||
        working-directory: docker/docker/backend
 | 
					        working-directory: docker/docker/backend
 | 
				
			||||||
@ -155,6 +155,10 @@
 | 
				
			|||||||
    "API": "https://mempool.space/api/v1/services",
 | 
					    "API": "https://mempool.space/api/v1/services",
 | 
				
			||||||
    "ACCELERATIONS": false
 | 
					    "ACCELERATIONS": false
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  "STRATUM": {
 | 
				
			||||||
 | 
					    "ENABLED": false,
 | 
				
			||||||
 | 
					    "API": "http://localhost:1234"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  "FIAT_PRICE": {
 | 
					  "FIAT_PRICE": {
 | 
				
			||||||
    "ENABLED": true,
 | 
					    "ENABLED": true,
 | 
				
			||||||
    "PAID": false,
 | 
					    "PAID": false,
 | 
				
			||||||
 | 
				
			|||||||
@ -151,5 +151,9 @@
 | 
				
			|||||||
    "ENABLED": true,
 | 
					    "ENABLED": true,
 | 
				
			||||||
    "PAID": false,
 | 
					    "PAID": false,
 | 
				
			||||||
    "API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
 | 
					    "API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "STRATUM": {
 | 
				
			||||||
 | 
					    "ENABLED": false,
 | 
				
			||||||
 | 
					    "API": "http://localhost:1234"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -159,6 +159,11 @@ describe('Mempool Backend Config', () => {
 | 
				
			|||||||
        PAID: false,
 | 
					        PAID: false,
 | 
				
			||||||
        API_KEY: '',
 | 
					        API_KEY: '',
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(config.STRATUM).toStrictEqual({
 | 
				
			||||||
 | 
					        ENABLED: false,
 | 
				
			||||||
 | 
					        API: 'http://localhost:1234',
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -119,7 +119,11 @@ class RbfCache {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
 | 
					  public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
 | 
				
			||||||
    if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
 | 
					    if ( !newTxExtended
 | 
				
			||||||
 | 
					      || !replaced?.length
 | 
				
			||||||
 | 
					      || this.txs.has(newTxExtended.txid)
 | 
				
			||||||
 | 
					      || !(replaced.some(tx => !this.replacedBy.has(tx.txid)))
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										105
									
								
								backend/src/api/services/stratum.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								backend/src/api/services/stratum.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,105 @@
 | 
				
			|||||||
 | 
					import { WebSocket } from 'ws';
 | 
				
			||||||
 | 
					import logger from '../../logger';
 | 
				
			||||||
 | 
					import config from '../../config';
 | 
				
			||||||
 | 
					import websocketHandler from '../websocket-handler';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface StratumJob {
 | 
				
			||||||
 | 
					  pool: number;
 | 
				
			||||||
 | 
					  height: number;
 | 
				
			||||||
 | 
					  coinbase: string;
 | 
				
			||||||
 | 
					  scriptsig: string;
 | 
				
			||||||
 | 
					  reward: number;
 | 
				
			||||||
 | 
					  jobId: string;
 | 
				
			||||||
 | 
					  extraNonce: string;
 | 
				
			||||||
 | 
					  extraNonce2Size: number;
 | 
				
			||||||
 | 
					  prevHash: string;
 | 
				
			||||||
 | 
					  coinbase1: string;
 | 
				
			||||||
 | 
					  coinbase2: string;
 | 
				
			||||||
 | 
					  merkleBranches: string[];
 | 
				
			||||||
 | 
					  version: string;
 | 
				
			||||||
 | 
					  bits: string;
 | 
				
			||||||
 | 
					  time: string;
 | 
				
			||||||
 | 
					  timestamp: number;
 | 
				
			||||||
 | 
					  cleanJobs: boolean;
 | 
				
			||||||
 | 
					  received: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function isStratumJob(obj: any): obj is StratumJob {
 | 
				
			||||||
 | 
					  return obj
 | 
				
			||||||
 | 
					    && typeof obj === 'object'
 | 
				
			||||||
 | 
					    && 'pool' in obj
 | 
				
			||||||
 | 
					    && 'prevHash' in obj
 | 
				
			||||||
 | 
					    && 'height' in obj
 | 
				
			||||||
 | 
					    && 'received' in obj
 | 
				
			||||||
 | 
					    && 'version' in obj
 | 
				
			||||||
 | 
					    && 'timestamp' in obj
 | 
				
			||||||
 | 
					    && 'bits' in obj
 | 
				
			||||||
 | 
					    && 'merkleBranches' in obj
 | 
				
			||||||
 | 
					    && 'cleanJobs' in obj;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StratumApi {
 | 
				
			||||||
 | 
					  private ws: WebSocket | null = null;
 | 
				
			||||||
 | 
					  private runWebsocketLoop: boolean = false;
 | 
				
			||||||
 | 
					  private startedWebsocketLoop: boolean = false;
 | 
				
			||||||
 | 
					  private websocketConnected: boolean = false;
 | 
				
			||||||
 | 
					  private jobs: Record<string, StratumJob> = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public constructor() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public getJobs(): Record<string, StratumJob> {
 | 
				
			||||||
 | 
					    return this.jobs;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private handleWebsocketMessage(msg: any): void {
 | 
				
			||||||
 | 
					    if (isStratumJob(msg)) {
 | 
				
			||||||
 | 
					      this.jobs[msg.pool] = msg;
 | 
				
			||||||
 | 
					      websocketHandler.handleNewStratumJob(this.jobs[msg.pool]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public async connectWebsocket(): Promise<void> {
 | 
				
			||||||
 | 
					    if (!config.STRATUM.ENABLED) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this.runWebsocketLoop = true;
 | 
				
			||||||
 | 
					    if (this.startedWebsocketLoop) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    while (this.runWebsocketLoop) {
 | 
				
			||||||
 | 
					      this.startedWebsocketLoop = true;
 | 
				
			||||||
 | 
					      if (!this.ws) {
 | 
				
			||||||
 | 
					        this.ws = new WebSocket(`${config.STRATUM.API}`);
 | 
				
			||||||
 | 
					        this.websocketConnected = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.ws.on('open', () => {
 | 
				
			||||||
 | 
					          logger.info('Stratum websocket opened');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.ws.on('error', (error) => {
 | 
				
			||||||
 | 
					          logger.err('Stratum websocket error: ' + error);
 | 
				
			||||||
 | 
					          this.ws = null;
 | 
				
			||||||
 | 
					          this.websocketConnected = false;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.ws.on('close', () => {
 | 
				
			||||||
 | 
					          logger.info('Stratum websocket closed');
 | 
				
			||||||
 | 
					          this.ws = null;
 | 
				
			||||||
 | 
					          this.websocketConnected = false;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.ws.on('message', (data, isBinary) => {
 | 
				
			||||||
 | 
					          try {
 | 
				
			||||||
 | 
					            const parsedMsg = JSON.parse((isBinary ? data : data.toString()) as string);
 | 
				
			||||||
 | 
					            this.handleWebsocketMessage(parsedMsg);
 | 
				
			||||||
 | 
					          } catch (e) {
 | 
				
			||||||
 | 
					            logger.warn('Failed to parse stratum websocket message: ' + (e instanceof Error ? e.message : e));
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      await new Promise(resolve => setTimeout(resolve, 5000));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default new StratumApi();
 | 
				
			||||||
@ -38,6 +38,7 @@ interface AddressTransactions {
 | 
				
			|||||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
 | 
					import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
 | 
				
			||||||
import { calculateMempoolTxCpfp } from './cpfp';
 | 
					import { calculateMempoolTxCpfp } from './cpfp';
 | 
				
			||||||
import { getRecentFirstSeen } from '../utils/file-read';
 | 
					import { getRecentFirstSeen } from '../utils/file-read';
 | 
				
			||||||
 | 
					import stratumApi, { StratumJob } from './services/stratum';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// valid 'want' subscriptions
 | 
					// valid 'want' subscriptions
 | 
				
			||||||
const wantable = [
 | 
					const wantable = [
 | 
				
			||||||
@ -403,6 +404,16 @@ class WebsocketHandler {
 | 
				
			|||||||
            delete client['track-mempool'];
 | 
					            delete client['track-mempool'];
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (parsedMessage && parsedMessage['track-stratum'] != null) {
 | 
				
			||||||
 | 
					            if (parsedMessage['track-stratum']) {
 | 
				
			||||||
 | 
					              const sub = parsedMessage['track-stratum'];
 | 
				
			||||||
 | 
					              client['track-stratum'] = sub;
 | 
				
			||||||
 | 
					              response['stratumJobs'] = this.socketData['stratumJobs'];
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              client['track-stratum'] = false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if (Object.keys(response).length) {
 | 
					          if (Object.keys(response).length) {
 | 
				
			||||||
            client.send(this.serializeResponse(response));
 | 
					            client.send(this.serializeResponse(response));
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
@ -1384,6 +1395,23 @@ class WebsocketHandler {
 | 
				
			|||||||
    await statistics.runStatistics();
 | 
					    await statistics.runStatistics();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public handleNewStratumJob(job: StratumJob): void {
 | 
				
			||||||
 | 
					    this.updateSocketDataFields({ 'stratumJobs': stratumApi.getJobs() });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const server of this.webSocketServers) {
 | 
				
			||||||
 | 
					      server.clients.forEach((client) => {
 | 
				
			||||||
 | 
					        if (client.readyState !== WebSocket.OPEN) {
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (client['track-stratum'] && (client['track-stratum'] === 'all' || client['track-stratum'] === job.pool)) {
 | 
				
			||||||
 | 
					          client.send(JSON.stringify({
 | 
				
			||||||
 | 
					            'stratumJob': job
 | 
				
			||||||
 | 
					        }));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // takes a dictionary of JSON serialized values
 | 
					  // takes a dictionary of JSON serialized values
 | 
				
			||||||
  // and zips it together into a valid JSON object
 | 
					  // and zips it together into a valid JSON object
 | 
				
			||||||
  private serializeResponse(response): string {
 | 
					  private serializeResponse(response): string {
 | 
				
			||||||
 | 
				
			|||||||
@ -165,6 +165,10 @@ interface IConfig {
 | 
				
			|||||||
  WALLETS: {
 | 
					  WALLETS: {
 | 
				
			||||||
    ENABLED: boolean;
 | 
					    ENABLED: boolean;
 | 
				
			||||||
    WALLETS: string[];
 | 
					    WALLETS: string[];
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  STRATUM: {
 | 
				
			||||||
 | 
					    ENABLED: boolean;
 | 
				
			||||||
 | 
					    API: string;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -332,6 +336,10 @@ const defaults: IConfig = {
 | 
				
			|||||||
    'ENABLED': false,
 | 
					    'ENABLED': false,
 | 
				
			||||||
    'WALLETS': [],
 | 
					    'WALLETS': [],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  'STRATUM': {
 | 
				
			||||||
 | 
					    'ENABLED': false,
 | 
				
			||||||
 | 
					    'API': 'http://localhost:1234',
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Config implements IConfig {
 | 
					class Config implements IConfig {
 | 
				
			||||||
@ -354,6 +362,7 @@ class Config implements IConfig {
 | 
				
			|||||||
  REDIS: IConfig['REDIS'];
 | 
					  REDIS: IConfig['REDIS'];
 | 
				
			||||||
  FIAT_PRICE: IConfig['FIAT_PRICE'];
 | 
					  FIAT_PRICE: IConfig['FIAT_PRICE'];
 | 
				
			||||||
  WALLETS: IConfig['WALLETS'];
 | 
					  WALLETS: IConfig['WALLETS'];
 | 
				
			||||||
 | 
					  STRATUM: IConfig['STRATUM'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor() {
 | 
					  constructor() {
 | 
				
			||||||
    const configs = this.merge(configFromFile, defaults);
 | 
					    const configs = this.merge(configFromFile, defaults);
 | 
				
			||||||
@ -376,6 +385,7 @@ class Config implements IConfig {
 | 
				
			|||||||
    this.REDIS = configs.REDIS;
 | 
					    this.REDIS = configs.REDIS;
 | 
				
			||||||
    this.FIAT_PRICE = configs.FIAT_PRICE;
 | 
					    this.FIAT_PRICE = configs.FIAT_PRICE;
 | 
				
			||||||
    this.WALLETS = configs.WALLETS;
 | 
					    this.WALLETS = configs.WALLETS;
 | 
				
			||||||
 | 
					    this.STRATUM = configs.STRATUM;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  merge = (...objects: object[]): IConfig => {
 | 
					  merge = (...objects: object[]): IConfig => {
 | 
				
			||||||
 | 
				
			|||||||
@ -48,6 +48,7 @@ import accelerationRoutes from './api/acceleration/acceleration.routes';
 | 
				
			|||||||
import aboutRoutes from './api/about.routes';
 | 
					import aboutRoutes from './api/about.routes';
 | 
				
			||||||
import mempoolBlocks from './api/mempool-blocks';
 | 
					import mempoolBlocks from './api/mempool-blocks';
 | 
				
			||||||
import walletApi from './api/services/wallets';
 | 
					import walletApi from './api/services/wallets';
 | 
				
			||||||
 | 
					import stratumApi from './api/services/stratum';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Server {
 | 
					class Server {
 | 
				
			||||||
  private wss: WebSocket.Server | undefined;
 | 
					  private wss: WebSocket.Server | undefined;
 | 
				
			||||||
@ -320,6 +321,9 @@ class Server {
 | 
				
			|||||||
    loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
 | 
					    loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    accelerationApi.connectWebsocket();
 | 
					    accelerationApi.connectWebsocket();
 | 
				
			||||||
 | 
					    if (config.STRATUM.ENABLED) {
 | 
				
			||||||
 | 
					      stratumApi.connectWebsocket();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setUpHttpApiRoutes(): void {
 | 
					  setUpHttpApiRoutes(): void {
 | 
				
			||||||
 | 
				
			|||||||
@ -148,6 +148,10 @@
 | 
				
			|||||||
    "API": "__MEMPOOL_SERVICES_API__",
 | 
					    "API": "__MEMPOOL_SERVICES_API__",
 | 
				
			||||||
    "ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
 | 
					    "ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  "STRATUM": {
 | 
				
			||||||
 | 
					    "ENABLED": __STRATUM_ENABLED__,
 | 
				
			||||||
 | 
					    "API": "__STRATUM_API__"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  "REDIS": {
 | 
					  "REDIS": {
 | 
				
			||||||
    "ENABLED": __REDIS_ENABLED__,
 | 
					    "ENABLED": __REDIS_ENABLED__,
 | 
				
			||||||
    "UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__",
 | 
					    "UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__",
 | 
				
			||||||
 | 
				
			|||||||
@ -149,6 +149,10 @@ __REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
 | 
				
			|||||||
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"}
 | 
					__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"}
 | 
				
			||||||
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
 | 
					__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# STRATUM
 | 
				
			||||||
 | 
					__STRATUM_ENABLED__=${STRATUM_ENABLED:=false}
 | 
				
			||||||
 | 
					__STRATUM_API__=${STRATUM_API:="http://localhost:1234"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# REDIS
 | 
					# REDIS
 | 
				
			||||||
__REDIS_ENABLED__=${REDIS_ENABLED:=false}
 | 
					__REDIS_ENABLED__=${REDIS_ENABLED:=false}
 | 
				
			||||||
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""}
 | 
					__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""}
 | 
				
			||||||
@ -300,6 +304,10 @@ sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.j
 | 
				
			|||||||
sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json
 | 
					sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json
 | 
				
			||||||
sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json
 | 
					sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# STRATUM
 | 
				
			||||||
 | 
					sed -i "s!__STRATUM_ENABLED__!${__STRATUM_ENABLED__}!g" mempool-config.json
 | 
				
			||||||
 | 
					sed -i "s!__STRATUM_API__!${__STRATUM_API__}!g" mempool-config.json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# REDIS
 | 
					# REDIS
 | 
				
			||||||
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
 | 
					sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
 | 
				
			||||||
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json
 | 
					sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json
 | 
				
			||||||
 | 
				
			|||||||
@ -344,7 +344,9 @@ describe('Mainnet', () => {
 | 
				
			|||||||
      cy.visit('/');
 | 
					      cy.visit('/');
 | 
				
			||||||
      cy.waitForSkeletonGone();
 | 
					      cy.waitForSkeletonGone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      cy.changeNetwork('testnet4');
 | 
					      //TODO(knorrium): add a check for the proxied server
 | 
				
			||||||
 | 
					      // cy.changeNetwork('testnet4');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      cy.changeNetwork('signet');
 | 
					      cy.changeNetwork('signet');
 | 
				
			||||||
      cy.changeNetwork('mainnet');
 | 
					      cy.changeNetwork('mainnet');
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
				
			|||||||
@ -27,5 +27,6 @@
 | 
				
			|||||||
  "ACCELERATOR": false,
 | 
					  "ACCELERATOR": false,
 | 
				
			||||||
  "ACCELERATOR_BUTTON": true,
 | 
					  "ACCELERATOR_BUTTON": true,
 | 
				
			||||||
  "PUBLIC_ACCELERATIONS": false,
 | 
					  "PUBLIC_ACCELERATIONS": false,
 | 
				
			||||||
 | 
					  "STRATUM_ENABLED": false,
 | 
				
			||||||
  "SERVICES_API": "https://mempool.space/api/v1/services"
 | 
					  "SERVICES_API": "https://mempool.space/api/v1/services"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -3,8 +3,10 @@ const fs = require('fs');
 | 
				
			|||||||
let PROXY_CONFIG = require('./proxy.conf');
 | 
					let PROXY_CONFIG = require('./proxy.conf');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
PROXY_CONFIG.forEach(entry => {
 | 
					PROXY_CONFIG.forEach(entry => {
 | 
				
			||||||
  entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space");
 | 
					  const hostname = process.env.CYPRESS_REROUTE_TESTNET === 'true' ? 'mempool-staging.fra.mempool.space' : 'node201.fmt.mempool.space';
 | 
				
			||||||
  entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space");
 | 
					  console.log(`e2e tests running against ${hostname}`);
 | 
				
			||||||
 | 
					  entry.target = entry.target.replace("mempool.space", hostname);
 | 
				
			||||||
 | 
					  entry.target = entry.target.replace("liquid.network", "liquid-staging.fmt.mempool.space");
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = PROXY_CONFIG;
 | 
					module.exports = PROXY_CONFIG;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
<div class="box card w-100" style="background: var(--box-bg)" id=acceleratePreviewAnchor>
 | 
					<div class="box card w-100 accelerate-checkout-inner" [class.input-disabled]="isCheckoutLocked > 0" style="background: var(--box-bg)" id=acceleratePreviewAnchor>
 | 
				
			||||||
  @if (accelerateError) {
 | 
					  @if (accelerateError) {
 | 
				
			||||||
    <div class="row mb-1 text-center">
 | 
					    <div class="row mb-1 text-center">
 | 
				
			||||||
      <div class="col-sm">
 | 
					      <div class="col-sm">
 | 
				
			||||||
@ -361,7 +361,7 @@
 | 
				
			|||||||
        <div class="row text-center justify-content-center mx-2">
 | 
					        <div class="row text-center justify-content-center mx-2">
 | 
				
			||||||
          <p i18n="accelerator.payment-to-mempool-space">Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank">{{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}</a></p>
 | 
					          <p i18n="accelerator.payment-to-mempool-space">Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank">{{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}</a></p>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        @if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) {
 | 
					        @if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay)) {
 | 
				
			||||||
          <div class="row">
 | 
					          <div class="row">
 | 
				
			||||||
            <div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
 | 
					            <div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
 | 
				
			||||||
              <p><ng-container i18n="accelerator.your-account-will-be-debited">Your account will be debited no more than</ng-container> <small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></p>
 | 
					              <p><ng-container i18n="accelerator.your-account-will-be-debited">Your account will be debited no more than</ng-container> <small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></p>
 | 
				
			||||||
@ -484,6 +484,11 @@
 | 
				
			|||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					        @if (isTokenizing > 0) {
 | 
				
			||||||
 | 
					          <div class="d-flex flex-row justify-content-center">
 | 
				
			||||||
 | 
					            <div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,13 @@
 | 
				
			|||||||
  color: var(--green)
 | 
					  color: var(--green)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.accelerate-checkout-inner {
 | 
				
			||||||
 | 
					  &.input-disabled {
 | 
				
			||||||
 | 
					    pointer-events: none;
 | 
				
			||||||
 | 
					    opacity: 0.75;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.paymentMethod {
 | 
					.paymentMethod {
 | 
				
			||||||
  padding: 10px;
 | 
					  padding: 10px;
 | 
				
			||||||
  background-color: var(--secondary);
 | 
					  background-color: var(--secondary);
 | 
				
			||||||
 | 
				
			|||||||
@ -76,6 +76,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  calculating = true;
 | 
					  calculating = true;
 | 
				
			||||||
  processing = false;
 | 
					  processing = false;
 | 
				
			||||||
 | 
					  isCheckoutLocked = 0; // reference counter, 0 = unlocked, >0 = locked
 | 
				
			||||||
 | 
					  isTokenizing = 0; // reference counter, 0 = false, >0 = true
 | 
				
			||||||
  selectedOption: 'wait' | 'accel';
 | 
					  selectedOption: 'wait' | 'accel';
 | 
				
			||||||
  cantPayReason = '';
 | 
					  cantPayReason = '';
 | 
				
			||||||
  quoteError = ''; // error fetching estimate or initial data
 | 
					  quoteError = ''; // error fetching estimate or initial data
 | 
				
			||||||
@ -154,7 +156,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
        this.accelerateError = null;
 | 
					        this.accelerateError = null;
 | 
				
			||||||
        this.timePaid = 0;
 | 
					        this.timePaid = 0;
 | 
				
			||||||
        this.btcpayInvoiceFailed = false;
 | 
					        this.btcpayInvoiceFailed = false;
 | 
				
			||||||
        this.moveToStep('summary');
 | 
					        this.moveToStep('summary', true);
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        this.auth = auth;
 | 
					        this.auth = auth;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -163,11 +165,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const urlParams = new URLSearchParams(window.location.search);
 | 
					    const urlParams = new URLSearchParams(window.location.search);
 | 
				
			||||||
    if (urlParams.get('cash_request_id')) { // Redirected from cashapp
 | 
					    if (urlParams.get('cash_request_id')) { // Redirected from cashapp
 | 
				
			||||||
      this.moveToStep('processing');
 | 
					      this.moveToStep('processing', true);
 | 
				
			||||||
      this.insertSquare();
 | 
					      this.insertSquare();
 | 
				
			||||||
      this.setupSquare();
 | 
					      this.setupSquare();
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      this.moveToStep('summary');
 | 
					      this.moveToStep('summary', true);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.conversionsSubscription = this.stateService.conversions$.subscribe(
 | 
					    this.conversionsSubscription = this.stateService.conversions$.subscribe(
 | 
				
			||||||
@ -192,14 +194,17 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    if (changes.accelerating && this.accelerating) {
 | 
					    if (changes.accelerating && this.accelerating) {
 | 
				
			||||||
      if (this.step === 'processing' || this.step === 'paid') {
 | 
					      if (this.step === 'processing' || this.step === 'paid') {
 | 
				
			||||||
        this.moveToStep('success');
 | 
					        this.moveToStep('success', true);
 | 
				
			||||||
      } else { // Edge case where the transaction gets accelerated by someone else or on another session
 | 
					      } else { // Edge case where the transaction gets accelerated by someone else or on another session
 | 
				
			||||||
        this.closeModal();
 | 
					        this.closeModal();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  moveToStep(step: CheckoutStep): void {
 | 
					  moveToStep(step: CheckoutStep, force: boolean = false): void {
 | 
				
			||||||
 | 
					    if (this.isCheckoutLocked > 0 && !force) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    this.processing = false;
 | 
					    this.processing = false;
 | 
				
			||||||
    this._step = step;
 | 
					    this._step = step;
 | 
				
			||||||
    if (this.timeoutTimer) {
 | 
					    if (this.timeoutTimer) {
 | 
				
			||||||
@ -242,7 +247,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  closeModal(): void {
 | 
					  closeModal(): void {
 | 
				
			||||||
    this.completed.emit(true);
 | 
					    this.completed.emit(true);
 | 
				
			||||||
    this.moveToStep('summary');
 | 
					    this.moveToStep('summary', true);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@ -393,7 +398,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
        this.audioService.playSound('ascend-chime-cartoon');
 | 
					        this.audioService.playSound('ascend-chime-cartoon');
 | 
				
			||||||
        this.showSuccess = true;
 | 
					        this.showSuccess = true;
 | 
				
			||||||
        this.estimateSubscription.unsubscribe();
 | 
					        this.estimateSubscription.unsubscribe();
 | 
				
			||||||
        this.moveToStep('paid');
 | 
					        this.moveToStep('paid', true);
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      error: (response) => {
 | 
					      error: (response) => {
 | 
				
			||||||
        this.processing = false;
 | 
					        this.processing = false;
 | 
				
			||||||
@ -503,56 +508,75 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
          this.loadingApplePay = false;
 | 
					          this.loadingApplePay = false;
 | 
				
			||||||
          applePayButton.addEventListener('click', async event => {
 | 
					          applePayButton.addEventListener('click', async event => {
 | 
				
			||||||
 | 
					            if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
 | 
				
			||||||
 | 
					              return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            event.preventDefault();
 | 
					            event.preventDefault();
 | 
				
			||||||
            const tokenResult = await this.applePay.tokenize();
 | 
					            try {
 | 
				
			||||||
            if (tokenResult?.status === 'OK') {
 | 
					              // lock the checkout UI and show a loading spinner until the square modals are finished
 | 
				
			||||||
              const card = tokenResult.details?.card;
 | 
					              this.isCheckoutLocked++;
 | 
				
			||||||
              if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
 | 
					              this.isTokenizing++;
 | 
				
			||||||
                console.error(`Cannot retreive payment card details`);
 | 
					              const tokenResult = await this.applePay.tokenize();
 | 
				
			||||||
                this.accelerateError = 'apple_pay_no_card_details';
 | 
					              if (tokenResult?.status === 'OK') {
 | 
				
			||||||
                this.processing = false;
 | 
					                const card = tokenResult.details?.card;
 | 
				
			||||||
                return;
 | 
					                if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
 | 
				
			||||||
              }
 | 
					                  console.error(`Cannot retreive payment card details`);
 | 
				
			||||||
              const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
 | 
					                  this.accelerateError = 'apple_pay_no_card_details';
 | 
				
			||||||
              this.servicesApiService.accelerateWithApplePay$(
 | 
					 | 
				
			||||||
                this.tx.txid,
 | 
					 | 
				
			||||||
                tokenResult.token,
 | 
					 | 
				
			||||||
                cardTag,
 | 
					 | 
				
			||||||
                `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
 | 
					 | 
				
			||||||
                costUSD
 | 
					 | 
				
			||||||
              ).subscribe({
 | 
					 | 
				
			||||||
                next: () => {
 | 
					 | 
				
			||||||
                  this.processing = false;
 | 
					                  this.processing = false;
 | 
				
			||||||
                  this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
 | 
					                  return;
 | 
				
			||||||
                  this.audioService.playSound('ascend-chime-cartoon');
 | 
					 | 
				
			||||||
                  if (this.applePay) {
 | 
					 | 
				
			||||||
                    this.applePay.destroy();
 | 
					 | 
				
			||||||
                  }
 | 
					 | 
				
			||||||
                  setTimeout(() => {
 | 
					 | 
				
			||||||
                    this.moveToStep('paid');
 | 
					 | 
				
			||||||
                  }, 1000);
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                error: (response) => {
 | 
					 | 
				
			||||||
                  this.processing = false;
 | 
					 | 
				
			||||||
                  this.accelerateError = response.error;
 | 
					 | 
				
			||||||
                  if (!(response.status === 403 && response.error === 'not_available')) {
 | 
					 | 
				
			||||||
                    setTimeout(() => {
 | 
					 | 
				
			||||||
                      // Reset everything by reloading the page :D, can be improved
 | 
					 | 
				
			||||||
                      const urlParams = new URLSearchParams(window.location.search);
 | 
					 | 
				
			||||||
                      window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
 | 
					 | 
				
			||||||
                    }, 3000);
 | 
					 | 
				
			||||||
                  }
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
              });
 | 
					                const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
 | 
				
			||||||
            } else {
 | 
					                // keep checkout in loading state until the acceleration request completes
 | 
				
			||||||
              this.processing = false;
 | 
					                this.isTokenizing++;
 | 
				
			||||||
              let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
 | 
					                this.isCheckoutLocked++;
 | 
				
			||||||
              if (tokenResult.errors) {
 | 
					                this.servicesApiService.accelerateWithApplePay$(
 | 
				
			||||||
                errorMessage += ` and errors: ${JSON.stringify(
 | 
					                  this.tx.txid,
 | 
				
			||||||
                  tokenResult.errors,
 | 
					                  tokenResult.token,
 | 
				
			||||||
                )}`;
 | 
					                  cardTag,
 | 
				
			||||||
 | 
					                  `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
 | 
				
			||||||
 | 
					                  costUSD
 | 
				
			||||||
 | 
					                ).subscribe({
 | 
				
			||||||
 | 
					                  next: () => {
 | 
				
			||||||
 | 
					                    this.processing = false;
 | 
				
			||||||
 | 
					                    this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
 | 
				
			||||||
 | 
					                    this.audioService.playSound('ascend-chime-cartoon');
 | 
				
			||||||
 | 
					                    if (this.applePay) {
 | 
				
			||||||
 | 
					                      this.applePay.destroy();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    setTimeout(() => {
 | 
				
			||||||
 | 
					                      this.isTokenizing--;
 | 
				
			||||||
 | 
					                      this.isCheckoutLocked--;
 | 
				
			||||||
 | 
					                      this.moveToStep('paid', true);
 | 
				
			||||||
 | 
					                    }, 1000);
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                  error: (response) => {
 | 
				
			||||||
 | 
					                    this.processing = false;
 | 
				
			||||||
 | 
					                    this.accelerateError = response.error;
 | 
				
			||||||
 | 
					                    if (!(response.status === 403 && response.error === 'not_available')) {
 | 
				
			||||||
 | 
					                      setTimeout(() => {
 | 
				
			||||||
 | 
					                        this.isTokenizing--;
 | 
				
			||||||
 | 
					                        this.isCheckoutLocked--;
 | 
				
			||||||
 | 
					                        // Reset everything by reloading the page :D, can be improved
 | 
				
			||||||
 | 
					                        const urlParams = new URLSearchParams(window.location.search);
 | 
				
			||||||
 | 
					                        window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
 | 
				
			||||||
 | 
					                      }, 3000);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                this.processing = false;
 | 
				
			||||||
 | 
					                let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
 | 
				
			||||||
 | 
					                if (tokenResult.errors) {
 | 
				
			||||||
 | 
					                  errorMessage += ` and errors: ${JSON.stringify(
 | 
				
			||||||
 | 
					                    tokenResult.errors,
 | 
				
			||||||
 | 
					                  )}`;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                throw new Error(errorMessage);
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
              throw new Error(errorMessage);
 | 
					            } finally {
 | 
				
			||||||
 | 
					              // always unlock the checkout once we're finished
 | 
				
			||||||
 | 
					              this.isTokenizing--;
 | 
				
			||||||
 | 
					              this.isCheckoutLocked--;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
        } catch (e) {
 | 
					        } catch (e) {
 | 
				
			||||||
@ -602,65 +626,84 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
        this.loadingGooglePay = false;
 | 
					        this.loadingGooglePay = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        document.getElementById('google-pay-button').addEventListener('click', async event => {
 | 
					        document.getElementById('google-pay-button').addEventListener('click', async event => {
 | 
				
			||||||
 | 
					          if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
          event.preventDefault();
 | 
					          event.preventDefault();
 | 
				
			||||||
          const tokenResult = await this.googlePay.tokenize();
 | 
					          try {
 | 
				
			||||||
          if (tokenResult?.status === 'OK') {
 | 
					            // lock the checkout UI and show a loading spinner until the square modals are finished
 | 
				
			||||||
            const card = tokenResult.details?.card;
 | 
					            this.isCheckoutLocked++;
 | 
				
			||||||
            if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
 | 
					            this.isTokenizing++;
 | 
				
			||||||
              console.error(`Cannot retreive payment card details`);
 | 
					            const tokenResult = await this.googlePay.tokenize();
 | 
				
			||||||
              this.accelerateError = 'apple_pay_no_card_details';
 | 
					            if (tokenResult?.status === 'OK') {
 | 
				
			||||||
              this.processing = false;
 | 
					              const card = tokenResult.details?.card;
 | 
				
			||||||
              return;
 | 
					              if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
 | 
				
			||||||
            }
 | 
					                console.error(`Cannot retreive payment card details`);
 | 
				
			||||||
            const verificationToken = await this.$verifyBuyer(this.payments, tokenResult.token, tokenResult.details, costUSD.toFixed(2));
 | 
					                this.accelerateError = 'apple_pay_no_card_details';
 | 
				
			||||||
            if (!verificationToken || !verificationToken.token) {
 | 
					 | 
				
			||||||
              console.error(`SCA verification failed`);
 | 
					 | 
				
			||||||
              this.accelerateError = 'SCA Verification Failed. Payment Declined.';
 | 
					 | 
				
			||||||
              this.processing = false;
 | 
					 | 
				
			||||||
              return;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
 | 
					 | 
				
			||||||
            this.servicesApiService.accelerateWithGooglePay$(
 | 
					 | 
				
			||||||
              this.tx.txid,
 | 
					 | 
				
			||||||
              tokenResult.token,
 | 
					 | 
				
			||||||
              verificationToken.token,
 | 
					 | 
				
			||||||
              cardTag,
 | 
					 | 
				
			||||||
              `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
 | 
					 | 
				
			||||||
              costUSD,
 | 
					 | 
				
			||||||
              verificationToken.userChallenged
 | 
					 | 
				
			||||||
            ).subscribe({
 | 
					 | 
				
			||||||
              next: () => {
 | 
					 | 
				
			||||||
                this.processing = false;
 | 
					                this.processing = false;
 | 
				
			||||||
                this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
 | 
					                return;
 | 
				
			||||||
                this.audioService.playSound('ascend-chime-cartoon');
 | 
					 | 
				
			||||||
                if (this.googlePay) {
 | 
					 | 
				
			||||||
                  this.googlePay.destroy();
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                setTimeout(() => {
 | 
					 | 
				
			||||||
                  this.moveToStep('paid');
 | 
					 | 
				
			||||||
                }, 1000);
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
              error: (response) => {
 | 
					 | 
				
			||||||
                this.processing = false;
 | 
					 | 
				
			||||||
                this.accelerateError = response.error;
 | 
					 | 
				
			||||||
                if (!(response.status === 403 && response.error === 'not_available')) {
 | 
					 | 
				
			||||||
                  setTimeout(() => {
 | 
					 | 
				
			||||||
                    // Reset everything by reloading the page :D, can be improved
 | 
					 | 
				
			||||||
                    const urlParams = new URLSearchParams(window.location.search);
 | 
					 | 
				
			||||||
                    window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
 | 
					 | 
				
			||||||
                  }, 3000);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            });
 | 
					              const verificationToken = await this.$verifyBuyer(this.payments, tokenResult.token, tokenResult.details, costUSD.toFixed(2));
 | 
				
			||||||
          } else {
 | 
					              if (!verificationToken || !verificationToken.token) {
 | 
				
			||||||
            this.processing = false;
 | 
					                console.error(`SCA verification failed`);
 | 
				
			||||||
            let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
 | 
					                this.accelerateError = 'SCA Verification Failed. Payment Declined.';
 | 
				
			||||||
            if (tokenResult.errors) {
 | 
					                this.processing = false;
 | 
				
			||||||
              errorMessage += ` and errors: ${JSON.stringify(
 | 
					                return;
 | 
				
			||||||
                tokenResult.errors,
 | 
					              }
 | 
				
			||||||
              )}`;
 | 
					              const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
 | 
				
			||||||
 | 
					              // keep checkout in loading state until the acceleration request completes
 | 
				
			||||||
 | 
					              this.isCheckoutLocked++;
 | 
				
			||||||
 | 
					              this.isTokenizing++;
 | 
				
			||||||
 | 
					              this.servicesApiService.accelerateWithGooglePay$(
 | 
				
			||||||
 | 
					                this.tx.txid,
 | 
				
			||||||
 | 
					                tokenResult.token,
 | 
				
			||||||
 | 
					                verificationToken.token,
 | 
				
			||||||
 | 
					                cardTag,
 | 
				
			||||||
 | 
					                `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
 | 
				
			||||||
 | 
					                costUSD,
 | 
				
			||||||
 | 
					                verificationToken.userChallenged
 | 
				
			||||||
 | 
					              ).subscribe({
 | 
				
			||||||
 | 
					                next: () => {
 | 
				
			||||||
 | 
					                  this.processing = false;
 | 
				
			||||||
 | 
					                  this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
 | 
				
			||||||
 | 
					                  this.audioService.playSound('ascend-chime-cartoon');
 | 
				
			||||||
 | 
					                  if (this.googlePay) {
 | 
				
			||||||
 | 
					                    this.googlePay.destroy();
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                  setTimeout(() => {
 | 
				
			||||||
 | 
					                    this.isTokenizing--;
 | 
				
			||||||
 | 
					                    this.isCheckoutLocked--;
 | 
				
			||||||
 | 
					                    this.moveToStep('paid', true);
 | 
				
			||||||
 | 
					                  }, 1000);
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                error: (response) => {
 | 
				
			||||||
 | 
					                  this.processing = false;
 | 
				
			||||||
 | 
					                  this.accelerateError = response.error;
 | 
				
			||||||
 | 
					                  this.isTokenizing--;
 | 
				
			||||||
 | 
					                  this.isCheckoutLocked--;
 | 
				
			||||||
 | 
					                  if (!(response.status === 403 && response.error === 'not_available')) {
 | 
				
			||||||
 | 
					                    setTimeout(() => {
 | 
				
			||||||
 | 
					                      // Reset everything by reloading the page :D, can be improved
 | 
				
			||||||
 | 
					                      const urlParams = new URLSearchParams(window.location.search);
 | 
				
			||||||
 | 
					                      window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
 | 
				
			||||||
 | 
					                    }, 3000);
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              this.processing = false;
 | 
				
			||||||
 | 
					              let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
 | 
				
			||||||
 | 
					              if (tokenResult.errors) {
 | 
				
			||||||
 | 
					                errorMessage += ` and errors: ${JSON.stringify(
 | 
				
			||||||
 | 
					                  tokenResult.errors,
 | 
				
			||||||
 | 
					                )}`;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              throw new Error(errorMessage);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            throw new Error(errorMessage);
 | 
					          } finally {
 | 
				
			||||||
 | 
					            // always unlock the checkout once we're finished
 | 
				
			||||||
 | 
					            this.isTokenizing--;
 | 
				
			||||||
 | 
					            this.isCheckoutLocked--;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -727,7 +770,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
                  this.cashAppPay.destroy();
 | 
					                  this.cashAppPay.destroy();
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                setTimeout(() => {
 | 
					                setTimeout(() => {
 | 
				
			||||||
                  this.moveToStep('paid');
 | 
					                  this.moveToStep('paid', true);
 | 
				
			||||||
                  if (window.history.replaceState) {
 | 
					                  if (window.history.replaceState) {
 | 
				
			||||||
                    const urlParams = new URLSearchParams(window.location.search);
 | 
					                    const urlParams = new URLSearchParams(window.location.search);
 | 
				
			||||||
                    window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ''));
 | 
					                    window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ''));
 | 
				
			||||||
@ -801,7 +844,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
    this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
 | 
					    this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
 | 
				
			||||||
    this.audioService.playSound('ascend-chime-cartoon');
 | 
					    this.audioService.playSound('ascend-chime-cartoon');
 | 
				
			||||||
    this.estimateSubscription.unsubscribe();
 | 
					    this.estimateSubscription.unsubscribe();
 | 
				
			||||||
    this.moveToStep('paid');
 | 
					    this.moveToStep('paid', true);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  isLoggedIn(): boolean {
 | 
					  isLoggedIn(): boolean {
 | 
				
			||||||
 | 
				
			|||||||
@ -478,25 +478,30 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  extendSummary(summary) {
 | 
					  extendSummary(summary) {
 | 
				
			||||||
    let extendedSummary = summary.slice();
 | 
					    const extendedSummary = summary.slice();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Add a point at today's date to make the graph end at the current time
 | 
					    // Add a point at today's date to make the graph end at the current time
 | 
				
			||||||
    extendedSummary.unshift({ time: Date.now() / 1000, value: 0 });
 | 
					    extendedSummary.unshift({ time: Date.now() / 1000, value: 0 });
 | 
				
			||||||
    extendedSummary.reverse();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let oneHour = 60 * 60;
 | 
					    let maxTime = Date.now() / 1000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const oneHour = 60 * 60;
 | 
				
			||||||
    // Fill gaps longer than interval
 | 
					    // Fill gaps longer than interval
 | 
				
			||||||
    for (let i = 0; i < extendedSummary.length - 1; i++) {
 | 
					    for (let i = 0; i < extendedSummary.length - 1; i++) {
 | 
				
			||||||
      let hours = Math.floor((extendedSummary[i + 1].time - extendedSummary[i].time) / oneHour);      
 | 
					      if (extendedSummary[i].time > maxTime) {
 | 
				
			||||||
 | 
					        extendedSummary[i].time = maxTime - 30;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      maxTime = extendedSummary[i].time;
 | 
				
			||||||
 | 
					      const hours = Math.floor((extendedSummary[i].time - extendedSummary[i + 1].time) / oneHour);
 | 
				
			||||||
      if (hours > 1) {
 | 
					      if (hours > 1) {
 | 
				
			||||||
        for (let j = 1; j < hours; j++) {
 | 
					        for (let j = 1; j < hours; j++) {
 | 
				
			||||||
          let newTime = extendedSummary[i].time + oneHour * j;
 | 
					          const newTime = extendedSummary[i].time - oneHour * j;
 | 
				
			||||||
          extendedSummary.splice(i + j, 0, { time: newTime, value: 0 });
 | 
					          extendedSummary.splice(i + j, 0, { time: newTime, value: 0 });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        i += hours - 1;
 | 
					        i += hours - 1;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return extendedSummary.reverse();
 | 
					    return extendedSummary;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -10,7 +10,7 @@
 | 
				
			|||||||
      <h1 class="m-0 pt-1 pt-md-0">{{ poolStats.pool.name }}</h1>
 | 
					      <h1 class="m-0 pt-1 pt-md-0">{{ poolStats.pool.name }}</h1>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="box">
 | 
					    <div class="box pool-details">
 | 
				
			||||||
      <div class="row">
 | 
					      <div class="row">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div class="col-lg-6">
 | 
					        <div class="col-lg-6">
 | 
				
			||||||
@ -173,7 +173,119 @@
 | 
				
			|||||||
    <div class="spinner-border text-light"></div>
 | 
					    <div class="spinner-border text-light"></div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <!-- Stratum Job -->
 | 
				
			||||||
 | 
					  <ng-container *ngIf="(job$ | async) as job;">
 | 
				
			||||||
 | 
					    <h2 i18n="pool.next_block">Next block</h2>
 | 
				
			||||||
 | 
					    <div class="box mb-3">
 | 
				
			||||||
 | 
					      <div class="row" >
 | 
				
			||||||
 | 
					        <div class="col">
 | 
				
			||||||
 | 
					          <table class="table table-borderless table-striped">
 | 
				
			||||||
 | 
					            <tbody>
 | 
				
			||||||
 | 
					              <tr>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                  <table class="job-table table table-xs table-borderless table-fixed table-data">
 | 
				
			||||||
 | 
					                    <thead>
 | 
				
			||||||
 | 
					                      <tr>
 | 
				
			||||||
 | 
					                        <th class="data-title clip text-center height" i18n="latest-blocks.height">Height</th>
 | 
				
			||||||
 | 
					                        <th class="data-title clip text-center expected" i18n="next-block.expected-time">Expected</th>
 | 
				
			||||||
 | 
					                        <th class="data-title clip text-center reward" i18n="latest-blocks.reward">Reward</th>
 | 
				
			||||||
 | 
					                        <th class="data-title clip text-center timestamp" i18n="next-block.timestamp">Timestamp</th>
 | 
				
			||||||
 | 
					                      </tr>
 | 
				
			||||||
 | 
					                    </thead>
 | 
				
			||||||
 | 
					                    <tbody>
 | 
				
			||||||
 | 
					                      <tr>
 | 
				
			||||||
 | 
					                        <td class="text-center height">
 | 
				
			||||||
 | 
					                          {{ job.height }}
 | 
				
			||||||
 | 
					                        </td>
 | 
				
			||||||
 | 
					                        <td class="text-center expected">
 | 
				
			||||||
 | 
					                          <ng-container *ngIf="(expectedBlockTime$ | async) as expectedBlockTime; else expectedPlaceholder">
 | 
				
			||||||
 | 
					                            <app-time kind="until" [time]="expectedBlockTime" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
 | 
				
			||||||
 | 
					                          </ng-container>
 | 
				
			||||||
 | 
					                          <ng-template #expectedPlaceholder>~</ng-template>
 | 
				
			||||||
 | 
					                        </td>
 | 
				
			||||||
 | 
					                        <td class="text-center reward">
 | 
				
			||||||
 | 
					                          <app-amount [satoshis]="job.reward"></app-amount>
 | 
				
			||||||
 | 
					                        </td>
 | 
				
			||||||
 | 
					                        <td class="text-center timestamp">
 | 
				
			||||||
 | 
					                          <app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="job.timestamp" [precision]="1" minUnit="minute" [hideTimeSince]="true"></app-timestamp>
 | 
				
			||||||
 | 
					                        </td>
 | 
				
			||||||
 | 
					                      </tr>
 | 
				
			||||||
 | 
					                    </tbody>
 | 
				
			||||||
 | 
					                  </table>
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					              </tr>
 | 
				
			||||||
 | 
					              <tr>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                  <table class="job-table table table-xs table-borderless table-fixed table-data">
 | 
				
			||||||
 | 
					                    <thead>
 | 
				
			||||||
 | 
					                      <tr>
 | 
				
			||||||
 | 
					                        <th class="data-title clip text-center coinbase" i18n="latest-blocks.coinbasetag">Coinbase tag</th>
 | 
				
			||||||
 | 
					                        <th class="data-title clip text-center clean" i18n="next-block.clean">Clean</th>
 | 
				
			||||||
 | 
					                        <th class="data-title clip text-center prevhash" i18n="next-block.prevhash">Prevhash</th>
 | 
				
			||||||
 | 
					                        <th class="data-title clip text-center job-received" i18n="next-block.job-received">Job Received</th>
 | 
				
			||||||
 | 
					                      </tr>
 | 
				
			||||||
 | 
					                    </thead>
 | 
				
			||||||
 | 
					                    <tbody>
 | 
				
			||||||
 | 
					                      <tr>
 | 
				
			||||||
 | 
					                        <td class="text-center coinbase">
 | 
				
			||||||
 | 
					                          {{ job.scriptsig | hex2ascii }}
 | 
				
			||||||
 | 
					                        </td>
 | 
				
			||||||
 | 
					                        <td class="text-center clean">
 | 
				
			||||||
 | 
					                          @if (job.cleanJobs) {
 | 
				
			||||||
 | 
					                            <fa-icon [icon]="['fas', 'check-circle']" [fixedWidth]="true"></fa-icon>
 | 
				
			||||||
 | 
					                          } @else {
 | 
				
			||||||
 | 
					                            <fa-icon [icon]="['fas', 'times-circle']" [fixedWidth]="true"></fa-icon>
 | 
				
			||||||
 | 
					                          }
 | 
				
			||||||
 | 
					                        </td>
 | 
				
			||||||
 | 
					                        <td class="text-center prevhash">
 | 
				
			||||||
 | 
					                          <a [routerLink]="['/block' | relativeUrl, job.prevHash]">
 | 
				
			||||||
 | 
					                            <app-truncate [text]="job.prevHash" [lastChars]="8"></app-truncate>
 | 
				
			||||||
 | 
					                          </a>
 | 
				
			||||||
 | 
					                        </td>
 | 
				
			||||||
 | 
					                        <td class="text-center job-received">
 | 
				
			||||||
 | 
					                          <app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="job.received / 1000" [precision]="1" minUnit="minute" [hideTimeSince]="true"></app-timestamp>
 | 
				
			||||||
 | 
					                        </td>
 | 
				
			||||||
 | 
					                      </tr>
 | 
				
			||||||
 | 
					                    </tbody>
 | 
				
			||||||
 | 
					                  </table>
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					              </tr>
 | 
				
			||||||
 | 
					              <tr>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                  <table class="stratum-table">
 | 
				
			||||||
 | 
					                    <thead>
 | 
				
			||||||
 | 
					                      <tr>
 | 
				
			||||||
 | 
					                        <th class="data-title clip text-center" [attr.colspan]="Math.max(job.merkleBranches.length, 12)">
 | 
				
			||||||
 | 
					                          <a class="title-link" href="" [routerLink]="['/stratum' | relativeUrl]">
 | 
				
			||||||
 | 
					                            Merkle Branches
 | 
				
			||||||
 | 
					                            <span> </span>
 | 
				
			||||||
 | 
					                            <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
 | 
				
			||||||
 | 
					                          </a>
 | 
				
			||||||
 | 
					                        </th>
 | 
				
			||||||
 | 
					                      </tr>
 | 
				
			||||||
 | 
					                    </thead>
 | 
				
			||||||
 | 
					                    <tbody>
 | 
				
			||||||
 | 
					                      <tr>
 | 
				
			||||||
 | 
					                        @for (branch of job.merkleBranches; track $index) {
 | 
				
			||||||
 | 
					                          <td class="merkle" [style.background-color]="branch ? '#' + branch.slice(0, 6) : ''"></td>
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        @for (_ of [].constructor(Math.max(0, 12 - job.merkleBranches.length)); track $index) {
 | 
				
			||||||
 | 
					                          <td class="merkle empty-branch"></td>
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                      </tr>
 | 
				
			||||||
 | 
					                    </tbody>
 | 
				
			||||||
 | 
					                  </table>
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					              </tr>
 | 
				
			||||||
 | 
					            </tbody>
 | 
				
			||||||
 | 
					          </table>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </ng-container>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <!-- Blocks list -->
 | 
					  <!-- Blocks list -->
 | 
				
			||||||
 | 
					  <h2 i18n="master-page.blocks">Blocks</h2>
 | 
				
			||||||
  <table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5"
 | 
					  <table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5"
 | 
				
			||||||
    [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()">
 | 
					    [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()">
 | 
				
			||||||
    <ng-container *ngIf="blocks$ | async as blocks; else skeleton">
 | 
					    <ng-container *ngIf="blocks$ | async as blocks; else skeleton">
 | 
				
			||||||
 | 
				
			|||||||
@ -49,111 +49,110 @@ div.scrollable {
 | 
				
			|||||||
  max-height: 75px;
 | 
					  max-height: 75px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.box {
 | 
					.pool-details {
 | 
				
			||||||
  padding-bottom: 5px;
 | 
					 | 
				
			||||||
  @media (min-width: 767.98px) {
 | 
					  @media (min-width: 767.98px) {
 | 
				
			||||||
    min-height: 187px;
 | 
					    min-height: 187px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
.label {
 | 
					  .label {
 | 
				
			||||||
  width: 25%;
 | 
					    width: 25%;
 | 
				
			||||||
  @media (min-width: 767.98px) {
 | 
					    @media (min-width: 767.98px) {
 | 
				
			||||||
    vertical-align: middle;
 | 
					      vertical-align: middle;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    @media (max-width: 767.98px) {
 | 
				
			||||||
 | 
					      font-weight: bold;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  @media (max-width: 767.98px) {
 | 
					  .label.addresses {
 | 
				
			||||||
    font-weight: bold;
 | 
					    vertical-align: top;
 | 
				
			||||||
 | 
					    padding-top: 25px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  .addresses-data {
 | 
				
			||||||
 | 
					    vertical-align: top;
 | 
				
			||||||
 | 
					    font-family: monospace;
 | 
				
			||||||
 | 
					    font-size: 14px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.label.addresses {
 | 
					 | 
				
			||||||
  vertical-align: top;
 | 
					 | 
				
			||||||
  padding-top: 25px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.addresses-data {
 | 
					 | 
				
			||||||
  vertical-align: top;
 | 
					 | 
				
			||||||
  font-family: monospace;
 | 
					 | 
				
			||||||
  font-size: 14px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
.data {
 | 
					  .data {
 | 
				
			||||||
  text-align: right;
 | 
					 | 
				
			||||||
  padding-left: 5%;
 | 
					 | 
				
			||||||
  @media (max-width: 992px) {
 | 
					 | 
				
			||||||
    text-align: left;
 | 
					 | 
				
			||||||
    padding-left: 12px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  @media (max-width: 450px) {
 | 
					 | 
				
			||||||
    text-align: right;
 | 
					    text-align: right;
 | 
				
			||||||
 | 
					    padding-left: 5%;
 | 
				
			||||||
 | 
					    @media (max-width: 992px) {
 | 
				
			||||||
 | 
					      text-align: left;
 | 
				
			||||||
 | 
					      padding-left: 12px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    @media (max-width: 450px) {
 | 
				
			||||||
 | 
					      text-align: right;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
.progress {
 | 
					  .progress {
 | 
				
			||||||
  background-color: var(--secondary);
 | 
					    background-color: var(--secondary);
 | 
				
			||||||
}
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.coinbase {
 | 
					  .coinbase {
 | 
				
			||||||
  width: 20%;
 | 
					 | 
				
			||||||
  @media (max-width: 875px) {
 | 
					 | 
				
			||||||
    display: none;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.height {
 | 
					 | 
				
			||||||
  width: 10%;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.timestamp {
 | 
					 | 
				
			||||||
  @media (max-width: 875px) {
 | 
					 | 
				
			||||||
    padding-left: 50px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  @media (max-width: 685px) {
 | 
					 | 
				
			||||||
    display: none;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.mined {
 | 
					 | 
				
			||||||
  width: 13%;
 | 
					 | 
				
			||||||
  @media (max-width: 1100px) {
 | 
					 | 
				
			||||||
    display: none;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.txs {
 | 
					 | 
				
			||||||
  padding-right: 40px;
 | 
					 | 
				
			||||||
  @media (max-width: 1100px) {
 | 
					 | 
				
			||||||
    padding-right: 10px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  @media (max-width: 875px) {
 | 
					 | 
				
			||||||
    padding-right: 20px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  @media (max-width: 567px) {
 | 
					 | 
				
			||||||
    padding-right: 10px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.size {
 | 
					 | 
				
			||||||
  width: 12%;
 | 
					 | 
				
			||||||
  @media (max-width: 1000px) {
 | 
					 | 
				
			||||||
    width: 15%;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  @media (max-width: 875px) {
 | 
					 | 
				
			||||||
    width: 20%;
 | 
					    width: 20%;
 | 
				
			||||||
 | 
					    @media (max-width: 875px) {
 | 
				
			||||||
 | 
					      display: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  @media (max-width: 650px) {
 | 
					 | 
				
			||||||
    width: 20%;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  @media (max-width: 450px) {
 | 
					 | 
				
			||||||
    display: none;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
.scriptmessage {
 | 
					  .height {
 | 
				
			||||||
	overflow: hidden;
 | 
					    width: 10%;
 | 
				
			||||||
	display: inline-block;
 | 
					  }
 | 
				
			||||||
	text-overflow: ellipsis;
 | 
					
 | 
				
			||||||
	vertical-align: middle;
 | 
					  .timestamp {
 | 
				
			||||||
	width: auto;
 | 
					    @media (max-width: 875px) {
 | 
				
			||||||
  text-align: left;
 | 
					      padding-left: 50px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    @media (max-width: 685px) {
 | 
				
			||||||
 | 
					      display: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .mined {
 | 
				
			||||||
 | 
					    width: 13%;
 | 
				
			||||||
 | 
					    @media (max-width: 1100px) {
 | 
				
			||||||
 | 
					      display: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .txs {
 | 
				
			||||||
 | 
					    padding-right: 40px;
 | 
				
			||||||
 | 
					    @media (max-width: 1100px) {
 | 
				
			||||||
 | 
					      padding-right: 10px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    @media (max-width: 875px) {
 | 
				
			||||||
 | 
					      padding-right: 20px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    @media (max-width: 567px) {
 | 
				
			||||||
 | 
					      padding-right: 10px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .size {
 | 
				
			||||||
 | 
					    width: 12%;
 | 
				
			||||||
 | 
					    @media (max-width: 1000px) {
 | 
				
			||||||
 | 
					      width: 15%;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    @media (max-width: 875px) {
 | 
				
			||||||
 | 
					      width: 20%;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    @media (max-width: 650px) {
 | 
				
			||||||
 | 
					      width: 20%;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    @media (max-width: 450px) {
 | 
				
			||||||
 | 
					      display: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .scriptmessage {
 | 
				
			||||||
 | 
					    overflow: hidden;
 | 
				
			||||||
 | 
					    display: inline-block;
 | 
				
			||||||
 | 
					    text-overflow: ellipsis;
 | 
				
			||||||
 | 
					    vertical-align: middle;
 | 
				
			||||||
 | 
					    width: auto;
 | 
				
			||||||
 | 
					    text-align: left;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.skeleton-loader {
 | 
					.skeleton-loader {
 | 
				
			||||||
@ -214,4 +213,55 @@ div.scrollable {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.taller-row {
 | 
					.taller-row {
 | 
				
			||||||
  height: 75px;
 | 
					  height: 75px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.stratum-table {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .merkle {
 | 
				
			||||||
 | 
					    width: 100px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .empty-branch {
 | 
				
			||||||
 | 
					    outline: solid 1px white;
 | 
				
			||||||
 | 
					    outline-offset: -1px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &::after {
 | 
				
			||||||
 | 
					      content: "";
 | 
				
			||||||
 | 
					      position: absolute;
 | 
				
			||||||
 | 
					      left: 0;
 | 
				
			||||||
 | 
					      top: 0;
 | 
				
			||||||
 | 
					      height: 100%;
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					      background: linear-gradient(to top left, transparent, transparent 48%, white 49%, white 51%, transparent 52%, transparent);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  td {
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    height: 2em;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.job-table {
 | 
				
			||||||
 | 
					  td, th {
 | 
				
			||||||
 | 
					    width: 25%;
 | 
				
			||||||
 | 
					    max-width: 25%;
 | 
				
			||||||
 | 
					    min-width: 25%;
 | 
				
			||||||
 | 
					    overflow: hidden;
 | 
				
			||||||
 | 
					    text-overflow: ellipsis;
 | 
				
			||||||
 | 
					    padding: 0.1rem 0.2rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media (max-width: 767.98px) {
 | 
				
			||||||
 | 
					    .expected, .timestamp, .clean, .job-received {
 | 
				
			||||||
 | 
					      display: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.title-link, .title-link:hover, .title-link:focus, .title-link:active {
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					  color: inherit;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -10,6 +10,9 @@ import { selectPowerOfTen } from '@app/bitcoin.utils';
 | 
				
			|||||||
import { formatNumber } from '@angular/common';
 | 
					import { formatNumber } from '@angular/common';
 | 
				
			||||||
import { SeoService } from '@app/services/seo.service';
 | 
					import { SeoService } from '@app/services/seo.service';
 | 
				
			||||||
import { HttpErrorResponse } from '@angular/common/http';
 | 
					import { HttpErrorResponse } from '@angular/common/http';
 | 
				
			||||||
 | 
					import { StratumJob } from '../../interfaces/websocket.interface';
 | 
				
			||||||
 | 
					import { WebsocketService } from '../../services/websocket.service';
 | 
				
			||||||
 | 
					import { MiningService } from '../../services/mining.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface AccelerationTotal {
 | 
					interface AccelerationTotal {
 | 
				
			||||||
  cost: number,
 | 
					  cost: number,
 | 
				
			||||||
@ -27,12 +30,16 @@ export class PoolComponent implements OnInit {
 | 
				
			|||||||
  @Input() left: number | string = 75;
 | 
					  @Input() left: number | string = 75;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  gfg = true;
 | 
					  gfg = true;
 | 
				
			||||||
 | 
					  stratumEnabled = this.stateService.env.STRATUM_ENABLED;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  formatNumber = formatNumber;
 | 
					  formatNumber = formatNumber;
 | 
				
			||||||
 | 
					  Math = Math;
 | 
				
			||||||
  slugSubscription: Subscription;
 | 
					  slugSubscription: Subscription;
 | 
				
			||||||
  poolStats$: Observable<PoolStat>;
 | 
					  poolStats$: Observable<PoolStat>;
 | 
				
			||||||
  blocks$: Observable<BlockExtended[]>;
 | 
					  blocks$: Observable<BlockExtended[]>;
 | 
				
			||||||
  oobFees$: Observable<AccelerationTotal[]>;
 | 
					  oobFees$: Observable<AccelerationTotal[]>;
 | 
				
			||||||
 | 
					  job$: Observable<StratumJob | null>;
 | 
				
			||||||
 | 
					  expectedBlockTime$: Observable<number>;
 | 
				
			||||||
  isLoading = true;
 | 
					  isLoading = true;
 | 
				
			||||||
  error: HttpErrorResponse | null = null;
 | 
					  error: HttpErrorResponse | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -53,6 +60,8 @@ export class PoolComponent implements OnInit {
 | 
				
			|||||||
    private apiService: ApiService,
 | 
					    private apiService: ApiService,
 | 
				
			||||||
    private route: ActivatedRoute,
 | 
					    private route: ActivatedRoute,
 | 
				
			||||||
    public stateService: StateService,
 | 
					    public stateService: StateService,
 | 
				
			||||||
 | 
					    private websocketService: WebsocketService,
 | 
				
			||||||
 | 
					    private miningService: MiningService,
 | 
				
			||||||
    private seoService: SeoService,
 | 
					    private seoService: SeoService,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.auditAvailable = this.stateService.env.AUDIT;
 | 
					    this.auditAvailable = this.stateService.env.AUDIT;
 | 
				
			||||||
@ -62,7 +71,7 @@ export class PoolComponent implements OnInit {
 | 
				
			|||||||
    this.slugSubscription = this.route.params.pipe(map((params) => params.slug)).subscribe((slug) => {
 | 
					    this.slugSubscription = this.route.params.pipe(map((params) => params.slug)).subscribe((slug) => {
 | 
				
			||||||
      this.isLoading = true;
 | 
					      this.isLoading = true;
 | 
				
			||||||
      this.blocks = [];
 | 
					      this.blocks = [];
 | 
				
			||||||
      this.chartOptions = {};  
 | 
					      this.chartOptions = {};
 | 
				
			||||||
      this.slug = slug;
 | 
					      this.slug = slug;
 | 
				
			||||||
      this.initializeObservables();
 | 
					      this.initializeObservables();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -129,6 +138,31 @@ export class PoolComponent implements OnInit {
 | 
				
			|||||||
      }),
 | 
					      }),
 | 
				
			||||||
      filter(oob => oob.length === 3 && oob[2].count > 0)
 | 
					      filter(oob => oob.length === 3 && oob[2].count > 0)
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.stratumEnabled) {
 | 
				
			||||||
 | 
					      this.job$ = combineLatest([
 | 
				
			||||||
 | 
					        this.poolStats$.pipe(
 | 
				
			||||||
 | 
					          tap((poolStats) => {
 | 
				
			||||||
 | 
					            this.websocketService.startTrackStratum(poolStats.pool.unique_id);
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        this.stateService.stratumJobs$
 | 
				
			||||||
 | 
					      ]).pipe(
 | 
				
			||||||
 | 
					        map(([poolStats, jobs]) => {
 | 
				
			||||||
 | 
					          return jobs[poolStats.pool.unique_id];
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.expectedBlockTime$ = combineLatest([
 | 
				
			||||||
 | 
					        this.miningService.getMiningStats('1w'),
 | 
				
			||||||
 | 
					        this.poolStats$,
 | 
				
			||||||
 | 
					        this.stateService.difficultyAdjustment$
 | 
				
			||||||
 | 
					      ]).pipe(
 | 
				
			||||||
 | 
					        map(([miningStats, poolStat, da]) => {
 | 
				
			||||||
 | 
					          return (da.timeAvg / ((poolStat.estimatedHashrate || 0) / (miningStats.lastEstimatedHashrate * 1_000_000_000_000_000_000))) + Date.now() + da.timeOffset;
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  prepareChartOptions(hashrate, share) {
 | 
					  prepareChartOptions(hashrate, share) {
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					<div class="container-xl" style="min-height: 335px">
 | 
				
			||||||
 | 
					  <h1 class="float-left" i18n="master-page.blocks">Stratum Jobs</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="clearfix"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div style="min-height: 295px">
 | 
				
			||||||
 | 
					    <table *ngIf="poolsReady && (rows$ | async) as rows;" class="stratum-table">
 | 
				
			||||||
 | 
					      <thead>
 | 
				
			||||||
 | 
					        <tr>
 | 
				
			||||||
 | 
					          <td class="height">Height</td>
 | 
				
			||||||
 | 
					          <td class="reward">Reward</td>
 | 
				
			||||||
 | 
					          <td class="tag">Coinbase Tag</td>
 | 
				
			||||||
 | 
					          <td class="merkle" [attr.colspan]="rows[0]?.merkleCells?.length || 4">
 | 
				
			||||||
 | 
					            Merkle Branches
 | 
				
			||||||
 | 
					          </td>
 | 
				
			||||||
 | 
					          <td class="pool">Pool</td>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					      </thead>
 | 
				
			||||||
 | 
					      <tbody>
 | 
				
			||||||
 | 
					        @for (row of rows; track row.job.pool) {
 | 
				
			||||||
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <td class="height">
 | 
				
			||||||
 | 
					              {{ row.job.height }}
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					            <td class="reward">
 | 
				
			||||||
 | 
					              <app-amount [satoshis]="row.job.reward"></app-amount>
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					            <td class="tag">
 | 
				
			||||||
 | 
					              {{ row.job.tag }}
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					            @for (cell of row.merkleCells; track $index) {
 | 
				
			||||||
 | 
					              <td class="merkle" [style.background-color]="cell.hash ? '#' + cell.hash.slice(0, 6) : ''">
 | 
				
			||||||
 | 
					                <div class="pipe-segment" [class]="pipeToClass(cell.type)"></div>
 | 
				
			||||||
 | 
					              </td>
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            <td class="pool">
 | 
				
			||||||
 | 
					              @if (pools[row.job.pool]) {
 | 
				
			||||||
 | 
					                <a class="badge" [routerLink]="[('/mining/pool/' + pools[row.job.pool].slug) | relativeUrl]">
 | 
				
			||||||
 | 
					                  <img class="pool-logo" [src]="'/resources/mining-pools/' + pools[row.job.pool].slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + pools[row.job.pool].name + ' mining pool'"> 
 | 
				
			||||||
 | 
					                  {{ pools[row.job.pool].name}}
 | 
				
			||||||
 | 
					                </a>
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					          </tr>
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      </tbody>
 | 
				
			||||||
 | 
					    </table>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@ -0,0 +1,108 @@
 | 
				
			|||||||
 | 
					.stratum-table {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					td {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  height: 2em;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &.height, &.reward, &.tag {
 | 
				
			||||||
 | 
					    padding: 0 5px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &.tag {
 | 
				
			||||||
 | 
					    max-width: 180px;
 | 
				
			||||||
 | 
					    overflow: hidden;
 | 
				
			||||||
 | 
					    text-overflow: ellipsis;
 | 
				
			||||||
 | 
					    white-space: nowrap;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  &.pool {
 | 
				
			||||||
 | 
					    padding-left: 5px;
 | 
				
			||||||
 | 
					    padding-right: 20px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &.merkle {
 | 
				
			||||||
 | 
					    width: 100px;
 | 
				
			||||||
 | 
					    .pipe-segment {
 | 
				
			||||||
 | 
					      position: absolute;
 | 
				
			||||||
 | 
					      border-color: white;
 | 
				
			||||||
 | 
					      box-sizing: content-box;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &.vertical {
 | 
				
			||||||
 | 
					        top: 0;
 | 
				
			||||||
 | 
					        right: 0;
 | 
				
			||||||
 | 
					        width: 50%;
 | 
				
			||||||
 | 
					        height: 100%;
 | 
				
			||||||
 | 
					        border-left: solid 4px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      &.horizontal {
 | 
				
			||||||
 | 
					        bottom: 0;
 | 
				
			||||||
 | 
					        left: 0;
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					        height: 50%;
 | 
				
			||||||
 | 
					        border-top: solid 4px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      &.branch-top {
 | 
				
			||||||
 | 
					        bottom: 0;
 | 
				
			||||||
 | 
					        right: 0;
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					        height: 50%;
 | 
				
			||||||
 | 
					        border-top: solid 4px;
 | 
				
			||||||
 | 
					        &::after {
 | 
				
			||||||
 | 
					          content: "";
 | 
				
			||||||
 | 
					          position: absolute;
 | 
				
			||||||
 | 
					          box-sizing: content-box;
 | 
				
			||||||
 | 
					          top: -4px;
 | 
				
			||||||
 | 
					          right: 0px;
 | 
				
			||||||
 | 
					          bottom: 0;
 | 
				
			||||||
 | 
					          width: 50%;
 | 
				
			||||||
 | 
					          border-top: solid 4px;
 | 
				
			||||||
 | 
					          border-left: solid 4px;
 | 
				
			||||||
 | 
					          border-top-left-radius: 5px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      &.branch-mid {
 | 
				
			||||||
 | 
					        bottom: 0;
 | 
				
			||||||
 | 
					        right: 0px;
 | 
				
			||||||
 | 
					        width: 50%;
 | 
				
			||||||
 | 
					        height: 100%;
 | 
				
			||||||
 | 
					        border-left: solid 4px;
 | 
				
			||||||
 | 
					        &::after {
 | 
				
			||||||
 | 
					          content: "";
 | 
				
			||||||
 | 
					          position: absolute;
 | 
				
			||||||
 | 
					          box-sizing: content-box;
 | 
				
			||||||
 | 
					          top: -4px;
 | 
				
			||||||
 | 
					          left: -4px;
 | 
				
			||||||
 | 
					          width: 100%;
 | 
				
			||||||
 | 
					          height: 50%;
 | 
				
			||||||
 | 
					          border-bottom: solid 4px;
 | 
				
			||||||
 | 
					          border-left: solid 4px;
 | 
				
			||||||
 | 
					          border-bottom-left-radius: 5px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      &.branch-end {
 | 
				
			||||||
 | 
					        top: -4px;
 | 
				
			||||||
 | 
					        right: 0;
 | 
				
			||||||
 | 
					        width: 50%;
 | 
				
			||||||
 | 
					        height: 50%;
 | 
				
			||||||
 | 
					        border-bottom-left-radius: 5px;
 | 
				
			||||||
 | 
					        border-bottom: solid 4px;
 | 
				
			||||||
 | 
					        border-left: solid 4px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.badge {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  color: #FFF;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pool-logo {
 | 
				
			||||||
 | 
					  width: 15px;
 | 
				
			||||||
 | 
					  height: 15px;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  top: -1px;
 | 
				
			||||||
 | 
					  margin-right: 2px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,224 @@
 | 
				
			|||||||
 | 
					import { Component, OnInit, ChangeDetectionStrategy, OnDestroy, ChangeDetectorRef } from '@angular/core';
 | 
				
			||||||
 | 
					import { StateService } from '../../../services/state.service';
 | 
				
			||||||
 | 
					import { WebsocketService } from '../../../services/websocket.service';
 | 
				
			||||||
 | 
					import { map, Observable } from 'rxjs';
 | 
				
			||||||
 | 
					import { StratumJob } from '../../../interfaces/websocket.interface';
 | 
				
			||||||
 | 
					import { MiningService } from '../../../services/mining.service';
 | 
				
			||||||
 | 
					import { SinglePoolStats } from '../../../interfaces/node-api.interface';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type MerkleCellType = ' ' | '┬' | '├' | '└' | '│' | '─' | 'leaf';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface TaggedStratumJob extends StratumJob {
 | 
				
			||||||
 | 
					  tag: string;
 | 
				
			||||||
 | 
					  merkleBranchIds: string[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface MerkleCell {
 | 
				
			||||||
 | 
					  hash: string;
 | 
				
			||||||
 | 
					  type: MerkleCellType;
 | 
				
			||||||
 | 
					  job?: TaggedStratumJob;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface MerkleTree {
 | 
				
			||||||
 | 
					  hash?: string;
 | 
				
			||||||
 | 
					  job: string;
 | 
				
			||||||
 | 
					  size: number;
 | 
				
			||||||
 | 
					  children?: MerkleTree[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface PoolRow {
 | 
				
			||||||
 | 
					  job: TaggedStratumJob;
 | 
				
			||||||
 | 
					  merkleCells: MerkleCell[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function parseTag(scriptSig: string): string {
 | 
				
			||||||
 | 
					  const hex = scriptSig.slice(8).replace(/6d6d.{64}/, '');
 | 
				
			||||||
 | 
					  const bytes: number[] = [];
 | 
				
			||||||
 | 
					  for (let i = 0; i < hex.length; i += 2) {
 | 
				
			||||||
 | 
					    bytes.push(parseInt(hex.substr(i, 2), 16));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  // eslint-disable-next-line no-control-regex
 | 
				
			||||||
 | 
					  const ascii = new TextDecoder('utf8').decode(Uint8Array.from(bytes)).replace(/\uFFFD/g, '').replace(/\\0/g, '').replace(/[\x00-\x1F\x7F-\x9F]/g, '');
 | 
				
			||||||
 | 
					  if (ascii.includes('/ViaBTC/')) {
 | 
				
			||||||
 | 
					    return '/ViaBTC/';
 | 
				
			||||||
 | 
					  } else if (ascii.includes('SpiderPool/')) {
 | 
				
			||||||
 | 
					    return 'SpiderPool/';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return (ascii.match(/\/.*\//)?.[0] || ascii).trim();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getMerkleBranchIds(merkleBranches: string[], numBranches: number): string[] {
 | 
				
			||||||
 | 
					  let lastHash = '';
 | 
				
			||||||
 | 
					  const ids: string[] = [];
 | 
				
			||||||
 | 
					  for (let i = 0; i < numBranches; i++) {
 | 
				
			||||||
 | 
					    if (merkleBranches[i]) {
 | 
				
			||||||
 | 
					      lastHash = merkleBranches[i];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    ids.push(`${i}-${lastHash}`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return ids;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-stratum-list',
 | 
				
			||||||
 | 
					  templateUrl: './stratum-list.component.html',
 | 
				
			||||||
 | 
					  styleUrls: ['./stratum-list.component.scss'],
 | 
				
			||||||
 | 
					  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class StratumList implements OnInit, OnDestroy {
 | 
				
			||||||
 | 
					  rows$: Observable<PoolRow[]>;
 | 
				
			||||||
 | 
					  pools: { [id: number]: SinglePoolStats } = {};
 | 
				
			||||||
 | 
					  poolsReady: boolean = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    private stateService: StateService,
 | 
				
			||||||
 | 
					    private websocketService: WebsocketService,
 | 
				
			||||||
 | 
					    private miningService: MiningService,
 | 
				
			||||||
 | 
					    private cd: ChangeDetectorRef,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ngOnInit(): void {
 | 
				
			||||||
 | 
					    this.websocketService.want(['stats', 'blocks', 'mempool-blocks']);
 | 
				
			||||||
 | 
					    this.miningService.getPools().subscribe(pools => {
 | 
				
			||||||
 | 
					      this.pools = {};
 | 
				
			||||||
 | 
					      for (const pool of pools) {
 | 
				
			||||||
 | 
					        this.pools[pool.unique_id] = pool;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this.poolsReady = true;
 | 
				
			||||||
 | 
					      this.cd.markForCheck();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    this.rows$ = this.stateService.stratumJobs$.pipe(
 | 
				
			||||||
 | 
					      map((jobs) => this.processJobs(jobs)),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    this.websocketService.startTrackStratum('all');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  processJobs(rawJobs: Record<string, StratumJob>): PoolRow[] {
 | 
				
			||||||
 | 
					    const numBranches = Math.max(...Object.values(rawJobs).map(job => job.merkleBranches.length));
 | 
				
			||||||
 | 
					    const jobs: Record<string, TaggedStratumJob> = {};
 | 
				
			||||||
 | 
					    for (const [id, job] of Object.entries(rawJobs)) {
 | 
				
			||||||
 | 
					      jobs[id] = { ...job, tag: parseTag(job.scriptsig), merkleBranchIds: getMerkleBranchIds(job.merkleBranches, numBranches) };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (Object.keys(jobs).length === 0) {
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let trees: MerkleTree[] = Object.keys(jobs).map(job => ({
 | 
				
			||||||
 | 
					      job,
 | 
				
			||||||
 | 
					      size: 1,
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // build tree from bottom up
 | 
				
			||||||
 | 
					    for (let col = numBranches - 1; col >= 0; col--) {
 | 
				
			||||||
 | 
					      const groups: Record<string, MerkleTree[]> = {};
 | 
				
			||||||
 | 
					      for (const tree of trees) {
 | 
				
			||||||
 | 
					        const branchId = jobs[tree.job].merkleBranchIds[col];
 | 
				
			||||||
 | 
					        if (!groups[branchId]) {
 | 
				
			||||||
 | 
					          groups[branchId] = [];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        groups[branchId].push(tree);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      trees = Object.values(groups).map(group => ({
 | 
				
			||||||
 | 
					        hash: jobs[group[0].job].merkleBranches[col],
 | 
				
			||||||
 | 
					        job: group[0].job,
 | 
				
			||||||
 | 
					        children: group,
 | 
				
			||||||
 | 
					        size: group.reduce((acc, tree) => acc + tree.size, 0),
 | 
				
			||||||
 | 
					      }));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // initialize grid of cells
 | 
				
			||||||
 | 
					    const rows: (MerkleCell | null)[][] = [];
 | 
				
			||||||
 | 
					    for (let i = 0; i < Object.keys(jobs).length; i++) {
 | 
				
			||||||
 | 
					      const row: (MerkleCell | null)[] = [];
 | 
				
			||||||
 | 
					      for (let j = 0; j <= numBranches; j++) {
 | 
				
			||||||
 | 
					        row.push(null);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      rows.push(row);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // fill in the cells
 | 
				
			||||||
 | 
					    let colTrees = [trees.sort((a, b) => {
 | 
				
			||||||
 | 
					      if (a.size !== b.size) {
 | 
				
			||||||
 | 
					        return b.size - a.size;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return a.job.localeCompare(b.job);
 | 
				
			||||||
 | 
					    })];
 | 
				
			||||||
 | 
					    for (let col = 0; col <= numBranches; col++) {
 | 
				
			||||||
 | 
					      let row = 0;
 | 
				
			||||||
 | 
					      const nextTrees: MerkleTree[][] = [];
 | 
				
			||||||
 | 
					      for (let g = 0; g < colTrees.length; g++) {
 | 
				
			||||||
 | 
					        for (let t = 0; t < colTrees[g].length; t++) {
 | 
				
			||||||
 | 
					          const tree = colTrees[g][t];
 | 
				
			||||||
 | 
					          const isFirstTree = (t === 0);
 | 
				
			||||||
 | 
					          const isLastTree = (t === colTrees[g].length - 1);
 | 
				
			||||||
 | 
					          for (let i = 0; i < tree.size; i++) {
 | 
				
			||||||
 | 
					            const isFirstCell = (i === 0);
 | 
				
			||||||
 | 
					            const isLeaf = (col === numBranches);
 | 
				
			||||||
 | 
					            rows[row][col] = {
 | 
				
			||||||
 | 
					              hash: tree.hash,
 | 
				
			||||||
 | 
					              job: isLeaf ? jobs[tree.job] : undefined,
 | 
				
			||||||
 | 
					              type: 'leaf',
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            if (col > 0) {
 | 
				
			||||||
 | 
					              rows[row][col - 1].type = getCellType(isFirstCell, isFirstTree, isLastTree);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            row++;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          if (tree.children) {
 | 
				
			||||||
 | 
					            nextTrees.push(tree.children.sort((a, b) => {
 | 
				
			||||||
 | 
					              if (a.size !== b.size) {
 | 
				
			||||||
 | 
					                return b.size - a.size;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              return a.job.localeCompare(b.job);
 | 
				
			||||||
 | 
					            }));
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      colTrees = nextTrees;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return rows.map(row => ({
 | 
				
			||||||
 | 
					      job: row[row.length - 1].job,
 | 
				
			||||||
 | 
					      merkleCells: row.slice(0, -1),
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pipeToClass(type: MerkleCellType): string {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      ' ': 'empty',
 | 
				
			||||||
 | 
					      '┬': 'branch-top',
 | 
				
			||||||
 | 
					      '├': 'branch-mid',
 | 
				
			||||||
 | 
					      '└': 'branch-end',
 | 
				
			||||||
 | 
					      '│': 'vertical',
 | 
				
			||||||
 | 
					      '─': 'horizontal',
 | 
				
			||||||
 | 
					      'leaf': 'leaf'
 | 
				
			||||||
 | 
					    }[type];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ngOnDestroy(): void {
 | 
				
			||||||
 | 
					    this.websocketService.stopTrackStratum();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getCellType(isFirstCell, isFirstTree, isLastTree): MerkleCellType {
 | 
				
			||||||
 | 
					  if (isFirstCell) {
 | 
				
			||||||
 | 
					    if (isFirstTree) {
 | 
				
			||||||
 | 
					      if (isLastTree) {
 | 
				
			||||||
 | 
					        return '─';
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        return '┬';
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else if (isLastTree) {
 | 
				
			||||||
 | 
					      return '└';
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return '├';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    if (isLastTree) {
 | 
				
			||||||
 | 
					      return ' ';
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return '│';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -24,6 +24,7 @@
 | 
				
			|||||||
          [height]="tx?.status?.block_height"
 | 
					          [height]="tx?.status?.block_height"
 | 
				
			||||||
          [replaced]="replaced"
 | 
					          [replaced]="replaced"
 | 
				
			||||||
          [removed]="this.rbfInfo?.mined && !this.tx?.status?.confirmed"
 | 
					          [removed]="this.rbfInfo?.mined && !this.tx?.status?.confirmed"
 | 
				
			||||||
 | 
					          [cached]="isCached"
 | 
				
			||||||
        ></app-confirmations>
 | 
					        ></app-confirmations>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </ng-container>
 | 
					    </ng-container>
 | 
				
			||||||
 | 
				
			|||||||
@ -21,6 +21,8 @@ export interface WebsocketResponse {
 | 
				
			|||||||
  rbfInfo?: RbfTree;
 | 
					  rbfInfo?: RbfTree;
 | 
				
			||||||
  rbfLatest?: RbfTree[];
 | 
					  rbfLatest?: RbfTree[];
 | 
				
			||||||
  rbfLatestSummary?: ReplacementInfo[];
 | 
					  rbfLatestSummary?: ReplacementInfo[];
 | 
				
			||||||
 | 
					  stratumJob?: StratumJob;
 | 
				
			||||||
 | 
					  stratumJobs?: Record<number, StratumJob>;
 | 
				
			||||||
  utxoSpent?: object;
 | 
					  utxoSpent?: object;
 | 
				
			||||||
  transactions?: TransactionStripped[];
 | 
					  transactions?: TransactionStripped[];
 | 
				
			||||||
  loadingIndicators?: ILoadingIndicators;
 | 
					  loadingIndicators?: ILoadingIndicators;
 | 
				
			||||||
@ -37,6 +39,7 @@ export interface WebsocketResponse {
 | 
				
			|||||||
  'track-rbf-summary'?: boolean;
 | 
					  'track-rbf-summary'?: boolean;
 | 
				
			||||||
  'track-accelerations'?: boolean;
 | 
					  'track-accelerations'?: boolean;
 | 
				
			||||||
  'track-wallet'?: string;
 | 
					  'track-wallet'?: string;
 | 
				
			||||||
 | 
					  'track-stratum'?: string | number;
 | 
				
			||||||
  'watch-mempool'?: boolean;
 | 
					  'watch-mempool'?: boolean;
 | 
				
			||||||
  'refresh-blocks'?: boolean;
 | 
					  'refresh-blocks'?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -150,3 +153,24 @@ export interface HealthCheckHost {
 | 
				
			|||||||
    electrs?: string;
 | 
					    electrs?: string;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface StratumJob {
 | 
				
			||||||
 | 
					  pool: number;
 | 
				
			||||||
 | 
					  height: number;
 | 
				
			||||||
 | 
					  coinbase: string;
 | 
				
			||||||
 | 
					  scriptsig: string;
 | 
				
			||||||
 | 
					  reward: number;
 | 
				
			||||||
 | 
					  jobId: string;
 | 
				
			||||||
 | 
					  extraNonce: string;
 | 
				
			||||||
 | 
					  extraNonce2Size: number;
 | 
				
			||||||
 | 
					  prevHash: string;
 | 
				
			||||||
 | 
					  coinbase1: string;
 | 
				
			||||||
 | 
					  coinbase2: string;
 | 
				
			||||||
 | 
					  merkleBranches: string[];
 | 
				
			||||||
 | 
					  version: string;
 | 
				
			||||||
 | 
					  bits: string;
 | 
				
			||||||
 | 
					  time: string;
 | 
				
			||||||
 | 
					  timestamp: number;
 | 
				
			||||||
 | 
					  cleanJobs: boolean;
 | 
				
			||||||
 | 
					  received: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -10,9 +10,10 @@ import { TestTransactionsComponent } from '@components/test-transactions/test-tr
 | 
				
			|||||||
import { CalculatorComponent } from '@components/calculator/calculator.component';
 | 
					import { CalculatorComponent } from '@components/calculator/calculator.component';
 | 
				
			||||||
import { BlocksList } from '@components/blocks-list/blocks-list.component';
 | 
					import { BlocksList } from '@components/blocks-list/blocks-list.component';
 | 
				
			||||||
import { RbfList } from '@components/rbf-list/rbf-list.component';
 | 
					import { RbfList } from '@components/rbf-list/rbf-list.component';
 | 
				
			||||||
 | 
					import { StratumList } from '@components/stratum/stratum-list/stratum-list.component';
 | 
				
			||||||
import { ServerHealthComponent } from '@components/server-health/server-health.component';
 | 
					import { ServerHealthComponent } from '@components/server-health/server-health.component';
 | 
				
			||||||
import { ServerStatusComponent } from '@components/server-health/server-status.component';
 | 
					import { ServerStatusComponent } from '@components/server-health/server-status.component';
 | 
				
			||||||
import { FaucetComponent } from '@components/faucet/faucet.component'
 | 
					import { FaucetComponent } from '@components/faucet/faucet.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const browserWindow = window || {};
 | 
					const browserWindow = window || {};
 | 
				
			||||||
// @ts-ignore
 | 
					// @ts-ignore
 | 
				
			||||||
@ -56,6 +57,16 @@ const routes: Routes = [
 | 
				
			|||||||
        path: 'rbf',
 | 
					        path: 'rbf',
 | 
				
			||||||
        component: RbfList,
 | 
					        component: RbfList,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      ...(browserWindowEnv.STRATUM_ENABLED ? [{
 | 
				
			||||||
 | 
					        path: 'stratum',
 | 
				
			||||||
 | 
					        component: StartComponent,
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            path: '',
 | 
				
			||||||
 | 
					            component: StratumList,
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      }] : []),
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        path: 'terms-of-service',
 | 
					        path: 'terms-of-service',
 | 
				
			||||||
        loadChildren: () => import('@components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule),
 | 
					        loadChildren: () => import('@components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule),
 | 
				
			||||||
 | 
				
			|||||||
@ -64,8 +64,8 @@ export class MiningService {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  /** 
 | 
					  /**
 | 
				
			||||||
   * Get names and slugs of all pools
 | 
					   * Get names and slugs of all pools
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  public getPools(): Observable<any[]> {
 | 
					  public getPools(): Observable<any[]> {
 | 
				
			||||||
@ -75,7 +75,6 @@ export class MiningService {
 | 
				
			|||||||
        return this.poolsData;
 | 
					        return this.poolsData;
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Set the hashrate power of ten we want to display
 | 
					   * Set the hashrate power of ten we want to display
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
 | 
					import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
 | 
				
			||||||
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
 | 
					import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
 | 
				
			||||||
import { AddressTxSummary, Transaction } from '@interfaces/electrs.interface';
 | 
					import { Transaction } from '@interfaces/electrs.interface';
 | 
				
			||||||
import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, isMempoolState } from '@interfaces/websocket.interface';
 | 
					import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, StratumJob, isMempoolState } from '@interfaces/websocket.interface';
 | 
				
			||||||
import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '@interfaces/node-api.interface';
 | 
					import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '@interfaces/node-api.interface';
 | 
				
			||||||
import { Router, NavigationStart } from '@angular/router';
 | 
					import { Router, NavigationStart } from '@angular/router';
 | 
				
			||||||
import { isPlatformBrowser } from '@angular/common';
 | 
					import { isPlatformBrowser } from '@angular/common';
 | 
				
			||||||
@ -81,6 +81,7 @@ export interface Env {
 | 
				
			|||||||
  ADDITIONAL_CURRENCIES: boolean;
 | 
					  ADDITIONAL_CURRENCIES: boolean;
 | 
				
			||||||
  GIT_COMMIT_HASH_MEMPOOL_SPACE?: string;
 | 
					  GIT_COMMIT_HASH_MEMPOOL_SPACE?: string;
 | 
				
			||||||
  PACKAGE_JSON_VERSION_MEMPOOL_SPACE?: string;
 | 
					  PACKAGE_JSON_VERSION_MEMPOOL_SPACE?: string;
 | 
				
			||||||
 | 
					  STRATUM_ENABLED: boolean;
 | 
				
			||||||
  SERVICES_API?: string;
 | 
					  SERVICES_API?: string;
 | 
				
			||||||
  customize?: Customization;
 | 
					  customize?: Customization;
 | 
				
			||||||
  PROD_DOMAINS: string[];
 | 
					  PROD_DOMAINS: string[];
 | 
				
			||||||
@ -123,6 +124,7 @@ const defaultEnv: Env = {
 | 
				
			|||||||
  'ACCELERATOR_BUTTON': true,
 | 
					  'ACCELERATOR_BUTTON': true,
 | 
				
			||||||
  'PUBLIC_ACCELERATIONS': false,
 | 
					  'PUBLIC_ACCELERATIONS': false,
 | 
				
			||||||
  'ADDITIONAL_CURRENCIES': false,
 | 
					  'ADDITIONAL_CURRENCIES': false,
 | 
				
			||||||
 | 
					  'STRATUM_ENABLED': false,
 | 
				
			||||||
  'SERVICES_API': 'https://mempool.space/api/v1/services',
 | 
					  'SERVICES_API': 'https://mempool.space/api/v1/services',
 | 
				
			||||||
  'PROD_DOMAINS': [],
 | 
					  'PROD_DOMAINS': [],
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@ -159,6 +161,8 @@ export class StateService {
 | 
				
			|||||||
  liveMempoolBlockTransactions$: Observable<{ block: number, transactions: { [txid: string]: TransactionStripped} }>;
 | 
					  liveMempoolBlockTransactions$: Observable<{ block: number, transactions: { [txid: string]: TransactionStripped} }>;
 | 
				
			||||||
  accelerations$ = new Subject<AccelerationDelta>();
 | 
					  accelerations$ = new Subject<AccelerationDelta>();
 | 
				
			||||||
  liveAccelerations$: Observable<Acceleration[]>;
 | 
					  liveAccelerations$: Observable<Acceleration[]>;
 | 
				
			||||||
 | 
					  stratumJobUpdate$ = new Subject<{ state: Record<string, StratumJob> } | { job: StratumJob }>();
 | 
				
			||||||
 | 
					  stratumJobs$ = new BehaviorSubject<Record<string, StratumJob>>({});
 | 
				
			||||||
  txConfirmed$ = new Subject<[string, BlockExtended]>();
 | 
					  txConfirmed$ = new Subject<[string, BlockExtended]>();
 | 
				
			||||||
  txReplaced$ = new Subject<ReplacedTransaction>();
 | 
					  txReplaced$ = new Subject<ReplacedTransaction>();
 | 
				
			||||||
  txRbfInfo$ = new Subject<RbfTree>();
 | 
					  txRbfInfo$ = new Subject<RbfTree>();
 | 
				
			||||||
@ -303,6 +307,24 @@ export class StateService {
 | 
				
			|||||||
      map((accMap) => Object.values(accMap).sort((a,b) => b.added - a.added))
 | 
					      map((accMap) => Object.values(accMap).sort((a,b) => b.added - a.added))
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.stratumJobUpdate$.pipe(
 | 
				
			||||||
 | 
					      scan((acc: Record<string, StratumJob>, update: { state: Record<string, StratumJob> } | { job: StratumJob }) => {
 | 
				
			||||||
 | 
					        if ('state' in update) {
 | 
				
			||||||
 | 
					          // Replace the entire state
 | 
				
			||||||
 | 
					          return update.state;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          // Update or create a single job entry
 | 
				
			||||||
 | 
					          return {
 | 
				
			||||||
 | 
					            ...acc,
 | 
				
			||||||
 | 
					            [update.job.pool]: update.job
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }, {}),
 | 
				
			||||||
 | 
					      shareReplay(1)
 | 
				
			||||||
 | 
					    ).subscribe(val => {
 | 
				
			||||||
 | 
					      this.stratumJobs$.next(val);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.networkChanged$.subscribe((network) => {
 | 
					    this.networkChanged$.subscribe((network) => {
 | 
				
			||||||
      this.transactions$ = new BehaviorSubject<TransactionStripped[]>(null);
 | 
					      this.transactions$ = new BehaviorSubject<TransactionStripped[]>(null);
 | 
				
			||||||
      this.blocksSubject$.next([]);
 | 
					      this.blocksSubject$.next([]);
 | 
				
			||||||
 | 
				
			|||||||
@ -36,6 +36,7 @@ export class WebsocketService {
 | 
				
			|||||||
  private isTrackingAccelerations: boolean = false;
 | 
					  private isTrackingAccelerations: boolean = false;
 | 
				
			||||||
  private isTrackingWallet: boolean = false;
 | 
					  private isTrackingWallet: boolean = false;
 | 
				
			||||||
  private trackingWalletName: string;
 | 
					  private trackingWalletName: string;
 | 
				
			||||||
 | 
					  private isTrackingStratum: string | number | false = false;
 | 
				
			||||||
  private trackingMempoolBlock: number;
 | 
					  private trackingMempoolBlock: number;
 | 
				
			||||||
  private trackingMempoolBlockNetwork: string;
 | 
					  private trackingMempoolBlockNetwork: string;
 | 
				
			||||||
  private stoppingTrackMempoolBlock: any | null = null;
 | 
					  private stoppingTrackMempoolBlock: any | null = null;
 | 
				
			||||||
@ -143,6 +144,9 @@ export class WebsocketService {
 | 
				
			|||||||
          if (this.isTrackingWallet) {
 | 
					          if (this.isTrackingWallet) {
 | 
				
			||||||
            this.startTrackingWallet(this.trackingWalletName);
 | 
					            this.startTrackingWallet(this.trackingWalletName);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					          if (this.isTrackingStratum !== false) {
 | 
				
			||||||
 | 
					            this.startTrackStratum(this.isTrackingStratum);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
          this.stateService.connectionState$.next(2);
 | 
					          this.stateService.connectionState$.next(2);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -289,6 +293,18 @@ export class WebsocketService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  startTrackStratum(pool: number | string) {
 | 
				
			||||||
 | 
					    this.websocketSubject.next({ 'track-stratum': pool });
 | 
				
			||||||
 | 
					    this.isTrackingStratum = pool;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  stopTrackStratum() {
 | 
				
			||||||
 | 
					    if (this.isTrackingStratum) {
 | 
				
			||||||
 | 
					      this.websocketSubject.next({ 'track-stratum': null });
 | 
				
			||||||
 | 
					      this.isTrackingStratum = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fetchStatistics(historicalDate: string) {
 | 
					  fetchStatistics(historicalDate: string) {
 | 
				
			||||||
    this.websocketSubject.next({ historicalDate });
 | 
					    this.websocketSubject.next({ historicalDate });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -512,6 +528,14 @@ export class WebsocketService {
 | 
				
			|||||||
      this.stateService.previousRetarget$.next(response.previousRetarget);
 | 
					      this.stateService.previousRetarget$.next(response.previousRetarget);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (response.stratumJobs) {
 | 
				
			||||||
 | 
					      this.stateService.stratumJobUpdate$.next({ state: response.stratumJobs });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (response.stratumJob) {
 | 
				
			||||||
 | 
					      this.stateService.stratumJobUpdate$.next({ job: response.stratumJob });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (response['tomahawk']) {
 | 
					    if (response['tomahawk']) {
 | 
				
			||||||
      this.stateService.serverHealth$.next(response['tomahawk']);
 | 
					      this.stateService.serverHealth$.next(response['tomahawk']);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -11,9 +11,9 @@
 | 
				
			|||||||
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && replaced">
 | 
					<ng-template [ngIf]="!hideUnconfirmed && !confirmations && replaced">
 | 
				
			||||||
  <button type="button" class="btn btn-sm btn-warning no-cursor {{buttonClass}}" i18n="transaction.replaced|Transaction replaced state">Replaced</button>
 | 
					  <button type="button" class="btn btn-sm btn-warning no-cursor {{buttonClass}}" i18n="transaction.replaced|Transaction replaced state">Replaced</button>
 | 
				
			||||||
</ng-template>
 | 
					</ng-template>
 | 
				
			||||||
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced && removed">
 | 
					<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced && (removed || cached)">
 | 
				
			||||||
  <button type="button" class="btn btn-sm btn-warning no-cursor {{buttonClass}}" i18n="transaction.audit.removed|Transaction removed state">Removed</button>
 | 
					  <button type="button" class="btn btn-sm btn-warning no-cursor {{buttonClass}}" i18n="transaction.audit.removed|Transaction removed state">Removed</button>
 | 
				
			||||||
</ng-template>
 | 
					</ng-template>
 | 
				
			||||||
<ng-template [ngIf]="!hideUnconfirmed && chainTip != null && !confirmations && !replaced && !removed">
 | 
					<ng-template [ngIf]="!hideUnconfirmed && chainTip != null && !confirmations && !replaced && !(removed || cached)">
 | 
				
			||||||
  <button type="button" class="btn btn-sm btn-danger no-cursor {{buttonClass}}" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
 | 
					  <button type="button" class="btn btn-sm btn-danger no-cursor {{buttonClass}}" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
 | 
				
			||||||
</ng-template>
 | 
					</ng-template>
 | 
				
			||||||
@ -12,6 +12,7 @@ export class ConfirmationsComponent implements OnChanges {
 | 
				
			|||||||
  @Input() height: number;
 | 
					  @Input() height: number;
 | 
				
			||||||
  @Input() replaced: boolean = false;
 | 
					  @Input() replaced: boolean = false;
 | 
				
			||||||
  @Input() removed: boolean = false;
 | 
					  @Input() removed: boolean = false;
 | 
				
			||||||
 | 
					  @Input() cached: boolean = false;
 | 
				
			||||||
  @Input() hideUnconfirmed: boolean = false;
 | 
					  @Input() hideUnconfirmed: boolean = false;
 | 
				
			||||||
  @Input() buttonClass: string = '';
 | 
					  @Input() buttonClass: string = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,10 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra
 | 
				
			|||||||
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
 | 
					import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
 | 
				
			||||||
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
 | 
					import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
 | 
				
			||||||
  faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
 | 
					  faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
 | 
				
			||||||
  faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, faCircleXmark, faCalendarCheck, faMoneyBillTrendUp } from '@fortawesome/free-solid-svg-icons';
 | 
					  faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft,
 | 
				
			||||||
 | 
					  faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck,
 | 
				
			||||||
 | 
					  faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline,
 | 
				
			||||||
 | 
					  faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faShareNodes } from '@fortawesome/free-solid-svg-icons';
 | 
				
			||||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
 | 
					import { InfiniteScrollModule } from 'ngx-infinite-scroll';
 | 
				
			||||||
import { MenuComponent } from '@components/menu/menu.component';
 | 
					import { MenuComponent } from '@components/menu/menu.component';
 | 
				
			||||||
import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component';
 | 
					import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component';
 | 
				
			||||||
@ -80,6 +83,7 @@ import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe';
 | 
				
			|||||||
import { DifficultyAdjustmentsTable } from '@components/difficulty-adjustments-table/difficulty-adjustments-table.components';
 | 
					import { DifficultyAdjustmentsTable } from '@components/difficulty-adjustments-table/difficulty-adjustments-table.components';
 | 
				
			||||||
import { BlocksList } from '@components/blocks-list/blocks-list.component';
 | 
					import { BlocksList } from '@components/blocks-list/blocks-list.component';
 | 
				
			||||||
import { RbfList } from '@components/rbf-list/rbf-list.component';
 | 
					import { RbfList } from '@components/rbf-list/rbf-list.component';
 | 
				
			||||||
 | 
					import { StratumList } from '@components/stratum/stratum-list/stratum-list.component';
 | 
				
			||||||
import { RewardStatsComponent } from '@components/reward-stats/reward-stats.component';
 | 
					import { RewardStatsComponent } from '@components/reward-stats/reward-stats.component';
 | 
				
			||||||
import { DataCyDirective } from '@app/data-cy.directive';
 | 
					import { DataCyDirective } from '@app/data-cy.directive';
 | 
				
			||||||
import { LoadingIndicatorComponent } from '@components/loading-indicator/loading-indicator.component';
 | 
					import { LoadingIndicatorComponent } from '@components/loading-indicator/loading-indicator.component';
 | 
				
			||||||
@ -198,6 +202,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
 | 
				
			|||||||
    DifficultyAdjustmentsTable,
 | 
					    DifficultyAdjustmentsTable,
 | 
				
			||||||
    BlocksList,
 | 
					    BlocksList,
 | 
				
			||||||
    RbfList,
 | 
					    RbfList,
 | 
				
			||||||
 | 
					    StratumList,
 | 
				
			||||||
    DataCyDirective,
 | 
					    DataCyDirective,
 | 
				
			||||||
    RewardStatsComponent,
 | 
					    RewardStatsComponent,
 | 
				
			||||||
    LoadingIndicatorComponent,
 | 
					    LoadingIndicatorComponent,
 | 
				
			||||||
@ -342,6 +347,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
 | 
				
			|||||||
    AmountShortenerPipe,
 | 
					    AmountShortenerPipe,
 | 
				
			||||||
    DifficultyAdjustmentsTable,
 | 
					    DifficultyAdjustmentsTable,
 | 
				
			||||||
    BlocksList,
 | 
					    BlocksList,
 | 
				
			||||||
 | 
					    StratumList,
 | 
				
			||||||
    DataCyDirective,
 | 
					    DataCyDirective,
 | 
				
			||||||
    RewardStatsComponent,
 | 
					    RewardStatsComponent,
 | 
				
			||||||
    LoadingIndicatorComponent,
 | 
					    LoadingIndicatorComponent,
 | 
				
			||||||
@ -452,5 +458,7 @@ export class SharedModule {
 | 
				
			|||||||
    library.addIcons(faCircleXmark);
 | 
					    library.addIcons(faCircleXmark);
 | 
				
			||||||
    library.addIcons(faCalendarCheck);
 | 
					    library.addIcons(faCalendarCheck);
 | 
				
			||||||
    library.addIcons(faMoneyBillTrendUp);
 | 
					    library.addIcons(faMoneyBillTrendUp);
 | 
				
			||||||
 | 
					    library.addIcons(faRobot);
 | 
				
			||||||
 | 
					    library.addIcons(faShareNodes);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user