Compare commits
	
		
			1 Commits
		
	
	
		
			master
			...
			orangesurf
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					35158de7c6 | 
							
								
								
									
										72
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										72
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@ -251,7 +251,17 @@ jobs:
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        module: ["mempool", "liquid", "testnet4"]
 | 
			
		||||
        module: ["mempool", "liquid"]
 | 
			
		||||
        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 }}
 | 
			
		||||
    steps:
 | 
			
		||||
@ -300,10 +310,8 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - name: Unzip assets before building (src/resources)
 | 
			
		||||
        run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video
 | 
			
		||||
 | 
			
		||||
      # mempool
 | 
			
		||||
      
 | 
			
		||||
      - name: Chrome browser tests (${{ matrix.module }})
 | 
			
		||||
        if: ${{ matrix.module == 'mempool' }}
 | 
			
		||||
        uses: cypress-io/github-action@v5
 | 
			
		||||
        with:
 | 
			
		||||
          tag: ${{ github.event_name }}
 | 
			
		||||
@ -314,9 +322,7 @@ jobs:
 | 
			
		||||
          wait-on-timeout: 120
 | 
			
		||||
          record: true
 | 
			
		||||
          parallel: true
 | 
			
		||||
          spec: |
 | 
			
		||||
            cypress/e2e/mainnet/*.spec.ts
 | 
			
		||||
            cypress/e2e/signet/*.spec.ts
 | 
			
		||||
          spec: ${{ matrix.spec }}
 | 
			
		||||
          group: Tests on Chrome (${{ matrix.module }})
 | 
			
		||||
          browser: "chrome"
 | 
			
		||||
          ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
 | 
			
		||||
@ -326,56 +332,6 @@ jobs:
 | 
			
		||||
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
          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:
 | 
			
		||||
    if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
 | 
			
		||||
    runs-on: "ubuntu-latest"
 | 
			
		||||
@ -403,4 +359,4 @@ jobs:
 | 
			
		||||
      - name: Validate JSON syntax
 | 
			
		||||
        run: |
 | 
			
		||||
          cat mempool-config.json | jq
 | 
			
		||||
        working-directory: docker/docker/backend
 | 
			
		||||
        working-directory: docker/docker/backend
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@ const config: Config.InitialOptions = {
 | 
			
		||||
  automock: false,
 | 
			
		||||
  collectCoverage: true,
 | 
			
		||||
  collectCoverageFrom: ["./src/**/**.ts"],
 | 
			
		||||
  coverageProvider: "v8",
 | 
			
		||||
  coverageProvider: "babel",
 | 
			
		||||
  coverageThreshold: {
 | 
			
		||||
    global: {
 | 
			
		||||
      lines: 1
 | 
			
		||||
 | 
			
		||||
@ -155,10 +155,6 @@
 | 
			
		||||
    "API": "https://mempool.space/api/v1/services",
 | 
			
		||||
    "ACCELERATIONS": false
 | 
			
		||||
  },
 | 
			
		||||
  "STRATUM": {
 | 
			
		||||
    "ENABLED": false,
 | 
			
		||||
    "API": "http://localhost:1234"
 | 
			
		||||
  },
 | 
			
		||||
  "FIAT_PRICE": {
 | 
			
		||||
    "ENABLED": true,
 | 
			
		||||
    "PAID": false,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										55
									
								
								backend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										55
									
								
								backend/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -10,6 +10,7 @@
 | 
			
		||||
      "hasInstallScript": true,
 | 
			
		||||
      "license": "GNU Affero General Public License v3.0",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/core": "^7.25.2",
 | 
			
		||||
        "@mempool/electrum-client": "1.1.9",
 | 
			
		||||
        "@types/node": "^18.15.3",
 | 
			
		||||
        "axios": "1.7.2",
 | 
			
		||||
@ -17,7 +18,7 @@
 | 
			
		||||
        "crypto-js": "~4.2.0",
 | 
			
		||||
        "express": "~4.21.1",
 | 
			
		||||
        "maxmind": "~4.3.11",
 | 
			
		||||
        "mysql2": "~3.12.0",
 | 
			
		||||
        "mysql2": "~3.11.0",
 | 
			
		||||
        "redis": "^4.7.0",
 | 
			
		||||
        "rust-gbt": "file:./rust-gbt",
 | 
			
		||||
        "socks-proxy-agent": "~7.0.0",
 | 
			
		||||
@ -25,6 +26,8 @@
 | 
			
		||||
        "ws": "~8.18.0"
 | 
			
		||||
      },
 | 
			
		||||
      "devDependencies": {
 | 
			
		||||
        "@babel/code-frame": "^7.18.6",
 | 
			
		||||
        "@babel/core": "^7.25.2",
 | 
			
		||||
        "@types/compression": "^1.7.2",
 | 
			
		||||
        "@types/crypto-js": "^4.1.1",
 | 
			
		||||
        "@types/express": "^4.17.17",
 | 
			
		||||
@ -5997,21 +6000,6 @@
 | 
			
		||||
        "yallist": "^3.0.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/lru.min": {
 | 
			
		||||
      "version": "1.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz",
 | 
			
		||||
      "integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "bun": ">=1.0.0",
 | 
			
		||||
        "deno": ">=1.30.0",
 | 
			
		||||
        "node": ">=8.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "type": "github",
 | 
			
		||||
        "url": "https://github.com/sponsors/wellwelwel"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/make-dir": {
 | 
			
		||||
      "version": "3.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
 | 
			
		||||
@ -6173,17 +6161,16 @@
 | 
			
		||||
      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/mysql2": {
 | 
			
		||||
      "version": "3.12.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz",
 | 
			
		||||
      "integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "version": "3.11.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.0.tgz",
 | 
			
		||||
      "integrity": "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "aws-ssl-profiles": "^1.1.1",
 | 
			
		||||
        "denque": "^2.1.0",
 | 
			
		||||
        "generate-function": "^2.3.1",
 | 
			
		||||
        "iconv-lite": "^0.6.3",
 | 
			
		||||
        "long": "^5.2.1",
 | 
			
		||||
        "lru.min": "^1.0.0",
 | 
			
		||||
        "lru-cache": "^8.0.0",
 | 
			
		||||
        "named-placeholders": "^1.1.3",
 | 
			
		||||
        "seq-queue": "^0.0.5",
 | 
			
		||||
        "sqlstring": "^2.3.2"
 | 
			
		||||
@ -6203,6 +6190,14 @@
 | 
			
		||||
        "node": ">=0.10.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/mysql2/node_modules/lru-cache": {
 | 
			
		||||
      "version": "8.0.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz",
 | 
			
		||||
      "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=16.14"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/named-placeholders": {
 | 
			
		||||
      "version": "1.1.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz",
 | 
			
		||||
@ -12218,11 +12213,6 @@
 | 
			
		||||
        "yallist": "^3.0.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "lru.min": {
 | 
			
		||||
      "version": "1.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz",
 | 
			
		||||
      "integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q=="
 | 
			
		||||
    },
 | 
			
		||||
    "make-dir": {
 | 
			
		||||
      "version": "3.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
 | 
			
		||||
@ -12337,16 +12327,16 @@
 | 
			
		||||
      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
 | 
			
		||||
    },
 | 
			
		||||
    "mysql2": {
 | 
			
		||||
      "version": "3.12.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz",
 | 
			
		||||
      "integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==",
 | 
			
		||||
      "version": "3.11.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.0.tgz",
 | 
			
		||||
      "integrity": "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "aws-ssl-profiles": "^1.1.1",
 | 
			
		||||
        "denque": "^2.1.0",
 | 
			
		||||
        "generate-function": "^2.3.1",
 | 
			
		||||
        "iconv-lite": "^0.6.3",
 | 
			
		||||
        "long": "^5.2.1",
 | 
			
		||||
        "lru.min": "^1.0.0",
 | 
			
		||||
        "lru-cache": "^8.0.0",
 | 
			
		||||
        "named-placeholders": "^1.1.3",
 | 
			
		||||
        "seq-queue": "^0.0.5",
 | 
			
		||||
        "sqlstring": "^2.3.2"
 | 
			
		||||
@ -12359,6 +12349,11 @@
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "safer-buffer": ">= 2.1.2 < 3.0.0"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "lru-cache": {
 | 
			
		||||
          "version": "8.0.5",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz",
 | 
			
		||||
          "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA=="
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
@ -39,6 +39,7 @@
 | 
			
		||||
    "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@babel/core": "^7.25.2",
 | 
			
		||||
    "@mempool/electrum-client": "1.1.9",
 | 
			
		||||
    "@types/node": "^18.15.3",
 | 
			
		||||
    "axios": "1.7.2",
 | 
			
		||||
@ -46,7 +47,7 @@
 | 
			
		||||
    "crypto-js": "~4.2.0",
 | 
			
		||||
    "express": "~4.21.1",
 | 
			
		||||
    "maxmind": "~4.3.11",
 | 
			
		||||
    "mysql2": "~3.12.0",
 | 
			
		||||
    "mysql2": "~3.11.0",
 | 
			
		||||
    "rust-gbt": "file:./rust-gbt",
 | 
			
		||||
    "redis": "^4.7.0",
 | 
			
		||||
    "socks-proxy-agent": "~7.0.0",
 | 
			
		||||
@ -54,6 +55,8 @@
 | 
			
		||||
    "ws": "~8.18.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@babel/code-frame": "^7.18.6",
 | 
			
		||||
    "@babel/core": "^7.25.2",
 | 
			
		||||
    "@types/compression": "^1.7.2",
 | 
			
		||||
    "@types/crypto-js": "^4.1.1",
 | 
			
		||||
    "@types/express": "^4.17.17",
 | 
			
		||||
 | 
			
		||||
@ -151,9 +151,5 @@
 | 
			
		||||
    "ENABLED": true,
 | 
			
		||||
    "PAID": false,
 | 
			
		||||
    "API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
 | 
			
		||||
  },
 | 
			
		||||
  "STRATUM": {
 | 
			
		||||
    "ENABLED": false,
 | 
			
		||||
    "API": "http://localhost:1234"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -159,11 +159,6 @@ describe('Mempool Backend Config', () => {
 | 
			
		||||
        PAID: false,
 | 
			
		||||
        API_KEY: '',
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      expect(config.STRATUM).toStrictEqual({
 | 
			
		||||
        ENABLED: false,
 | 
			
		||||
        API: 'http://localhost:1234',
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,10 +3,6 @@ import logger from '../../logger';
 | 
			
		||||
import bitcoinClient from './bitcoin-client';
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
 | 
			
		||||
const BLOCKHASH_REGEX = /^[a-f0-9]{64}$/i;
 | 
			
		||||
const TXID_REGEX = /^[a-f0-9]{64}$/i;
 | 
			
		||||
const RAW_TX_REGEX = /^[a-f0-9]{2,}$/i;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Define a set of routes used by the accelerator server
 | 
			
		||||
 * Those routes are not designed to be public
 | 
			
		||||
@ -14,7 +10,7 @@ const RAW_TX_REGEX = /^[a-f0-9]{2,}$/i;
 | 
			
		||||
class BitcoinBackendRoutes {
 | 
			
		||||
  private static tag = 'BitcoinBackendRoutes';
 | 
			
		||||
 | 
			
		||||
  public initRoutes(app: Application): void {
 | 
			
		||||
  public initRoutes(app: Application) {
 | 
			
		||||
    app
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry)
 | 
			
		||||
      .post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction)
 | 
			
		||||
@ -30,10 +26,10 @@ class BitcoinBackendRoutes {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Disable caching for bitcoin core routes
 | 
			
		||||
   *
 | 
			
		||||
   * @param req
 | 
			
		||||
   * @param res
 | 
			
		||||
   * @param next
 | 
			
		||||
   * 
 | 
			
		||||
   * @param req 
 | 
			
		||||
   * @param res 
 | 
			
		||||
   * @param next 
 | 
			
		||||
   */
 | 
			
		||||
  private disableCache(req: Request, res: Response, next: NextFunction): void  {
 | 
			
		||||
    res.setHeader('Pragma', 'no-cache');
 | 
			
		||||
@ -44,16 +40,16 @@ class BitcoinBackendRoutes {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Exeption handler to return proper details to the accelerator server
 | 
			
		||||
   *
 | 
			
		||||
   * @param e
 | 
			
		||||
   * @param fnName
 | 
			
		||||
   * @param res
 | 
			
		||||
   * 
 | 
			
		||||
   * @param e 
 | 
			
		||||
   * @param fnName 
 | 
			
		||||
   * @param res 
 | 
			
		||||
   */
 | 
			
		||||
  private static handleException(e: any, fnName: string, res: Response): void {
 | 
			
		||||
    if (typeof(e.code) === 'number') {
 | 
			
		||||
      res.status(400).send(JSON.stringify(e, ['code']));
 | 
			
		||||
    } else {
 | 
			
		||||
      const err = `unknown exception in ${fnName}`;
 | 
			
		||||
      res.status(400).send(JSON.stringify(e, ['code', 'message']));
 | 
			
		||||
    } else {     
 | 
			
		||||
      const err = `exception in ${fnName}. ${e}. Details: ${JSON.stringify(e, ['code', 'message'])}`; 
 | 
			
		||||
      logger.err(err, BitcoinBackendRoutes.tag);
 | 
			
		||||
      res.status(500).send(err);
 | 
			
		||||
    }
 | 
			
		||||
@ -62,13 +58,13 @@ class BitcoinBackendRoutes {
 | 
			
		||||
  private async $getMempoolEntry(req: Request, res: Response): Promise<void> {
 | 
			
		||||
    const txid = req.query.txid;
 | 
			
		||||
    try {
 | 
			
		||||
      if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) {
 | 
			
		||||
        res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`);
 | 
			
		||||
      if (typeof(txid) !== 'string' || txid.length !== 64) {
 | 
			
		||||
        res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const mempoolEntry = await bitcoinClient.getMempoolEntry(txid);
 | 
			
		||||
      if (!mempoolEntry) {
 | 
			
		||||
        res.status(404).send();
 | 
			
		||||
        res.status(404).send(`no mempool entry found for txid ${txid}`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      res.status(200).send(mempoolEntry);
 | 
			
		||||
@ -80,13 +76,13 @@ class BitcoinBackendRoutes {
 | 
			
		||||
  private async $decodeRawTransaction(req: Request, res: Response): Promise<void> {
 | 
			
		||||
    const rawTx = req.body.rawTx;
 | 
			
		||||
    try {
 | 
			
		||||
      if (typeof(rawTx) !== 'string' || !RAW_TX_REGEX.test(rawTx)) {
 | 
			
		||||
        res.status(400).send(`invalid param rawTx. must be a string of hexadecimal characters`);
 | 
			
		||||
      if (typeof(rawTx) !== 'string') {
 | 
			
		||||
        res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const decodedTx = await bitcoinClient.decodeRawTransaction(rawTx);
 | 
			
		||||
      if (!decodedTx) {
 | 
			
		||||
        res.status(400).send(`unable to decode rawTx`);
 | 
			
		||||
        res.status(400).send(`unable to decode rawTx ${rawTx}`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      res.status(200).send(decodedTx);
 | 
			
		||||
@ -99,23 +95,23 @@ class BitcoinBackendRoutes {
 | 
			
		||||
    const txid = req.query.txid;
 | 
			
		||||
    const verbose = req.query.verbose;
 | 
			
		||||
    try {
 | 
			
		||||
      if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) {
 | 
			
		||||
        res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`);
 | 
			
		||||
      if (typeof(txid) !== 'string' || txid.length !== 64) {
 | 
			
		||||
        res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (typeof(verbose) !== 'string') {
 | 
			
		||||
        res.status(400).send(`invalid param verbose. must be a string representing an integer`);
 | 
			
		||||
        res.status(400).send(`invalid param verbose ${verbose}. must be a string representing an integer`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const verboseNumber = parseInt(verbose, 10);
 | 
			
		||||
      if (typeof(verboseNumber) !== 'number') {
 | 
			
		||||
        res.status(400).send(`invalid param verbose. must be a valid integer`);
 | 
			
		||||
        res.status(400).send(`invalid param verbose ${verbose}. must be a valid integer`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const decodedTx = await bitcoinClient.getRawTransaction(txid, verboseNumber);
 | 
			
		||||
      if (!decodedTx) {
 | 
			
		||||
        res.status(400).send(`unable to get raw transaction`);
 | 
			
		||||
        res.status(400).send(`unable to get raw transaction for txid ${txid}`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      res.status(200).send(decodedTx);
 | 
			
		||||
@ -127,13 +123,13 @@ class BitcoinBackendRoutes {
 | 
			
		||||
  private async $sendRawTransaction(req: Request, res: Response): Promise<void> {
 | 
			
		||||
    const rawTx = req.body.rawTx;
 | 
			
		||||
    try {
 | 
			
		||||
      if (typeof(rawTx) !== 'string' || !RAW_TX_REGEX.test(rawTx)) {
 | 
			
		||||
        res.status(400).send(`invalid param rawTx. must be a string of hexadecimal characters`);
 | 
			
		||||
      if (typeof(rawTx) !== 'string') {
 | 
			
		||||
        res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const txHex = await bitcoinClient.sendRawTransaction(rawTx);
 | 
			
		||||
      if (!txHex) {
 | 
			
		||||
        res.status(400).send(`unable to send rawTx`);
 | 
			
		||||
        res.status(400).send(`unable to send rawTx ${rawTx}`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      res.status(200).send(txHex);
 | 
			
		||||
@ -145,13 +141,13 @@ class BitcoinBackendRoutes {
 | 
			
		||||
  private async $testMempoolAccept(req: Request, res: Response): Promise<void> {
 | 
			
		||||
    const rawTxs = req.body.rawTxs;
 | 
			
		||||
    try {
 | 
			
		||||
      if (typeof(rawTxs) !== 'object' || !Array.isArray(rawTxs) || rawTxs.some((tx) => typeof(tx) !== 'string' || !RAW_TX_REGEX.test(tx))) {
 | 
			
		||||
        res.status(400).send(`invalid param rawTxs. must be an array of strings of hexadecimal characters`);
 | 
			
		||||
      if (typeof(rawTxs) !== 'object') {
 | 
			
		||||
        res.status(400).send(`invalid param rawTxs ${JSON.stringify(rawTxs)}. must be an array of string`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const txHex = await bitcoinClient.testMempoolAccept(rawTxs);
 | 
			
		||||
      if (typeof(txHex) !== 'object' || txHex.length === 0) {
 | 
			
		||||
        res.status(400).send(`testmempoolaccept failed for raw txs, got an empty result`);
 | 
			
		||||
        res.status(400).send(`testmempoolaccept failed for raw txs ${JSON.stringify(rawTxs)}, got an empty result`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      res.status(200).send(txHex);
 | 
			
		||||
@ -164,18 +160,18 @@ class BitcoinBackendRoutes {
 | 
			
		||||
    const txid = req.query.txid;
 | 
			
		||||
    const verbose = req.query.verbose;
 | 
			
		||||
    try {
 | 
			
		||||
      if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) {
 | 
			
		||||
        res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`);
 | 
			
		||||
      if (typeof(txid) !== 'string' || txid.length !== 64) {
 | 
			
		||||
        res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (typeof(verbose) !== 'string' || (verbose !== 'true' && verbose !== 'false')) {
 | 
			
		||||
        res.status(400).send(`invalid param verbose. must be a string ('true' | 'false')`);
 | 
			
		||||
        res.status(400).send(`invalid param verbose ${verbose}. must be a string ('true' | 'false')`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
      const ancestors = await bitcoinClient.getMempoolAncestors(txid, verbose === 'true' ? true : false);
 | 
			
		||||
      if (!ancestors) {
 | 
			
		||||
        res.status(400).send(`unable to get mempool ancestors`);
 | 
			
		||||
        res.status(400).send(`unable to get mempool ancestors for txid ${txid}`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      res.status(200).send(ancestors);
 | 
			
		||||
@ -188,23 +184,23 @@ class BitcoinBackendRoutes {
 | 
			
		||||
    const blockHash = req.query.hash;
 | 
			
		||||
    const verbosity = req.query.verbosity;
 | 
			
		||||
    try {
 | 
			
		||||
      if (typeof(blockHash) !== 'string' || blockHash.length !== 64 || !BLOCKHASH_REGEX.test(blockHash)) {
 | 
			
		||||
        res.status(400).send(`invalid param blockHash. must be 64 hexadecimal characters`);
 | 
			
		||||
      if (typeof(blockHash) !== 'string' || blockHash.length !== 64) {
 | 
			
		||||
        res.status(400).send(`invalid param blockHash ${blockHash}. must be a string of 64 char`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (typeof(verbosity) !== 'string') {
 | 
			
		||||
        res.status(400).send(`invalid param verbosity. must be a string representing an integer`);
 | 
			
		||||
        res.status(400).send(`invalid param verbosity ${verbosity}. must be a string representing an integer`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const verbosityNumber = parseInt(verbosity, 10);
 | 
			
		||||
      if (typeof(verbosityNumber) !== 'number') {
 | 
			
		||||
        res.status(400).send(`invalid param verbosity. must be a valid integer`);
 | 
			
		||||
        res.status(400).send(`invalid param verbosity ${verbosity}. must be a valid integer`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const block = await bitcoinClient.getBlock(blockHash, verbosityNumber);
 | 
			
		||||
      if (!block) {
 | 
			
		||||
        res.status(400).send(`unable to get block`);
 | 
			
		||||
        res.status(400).send(`unable to get block for block hash ${blockHash}`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      res.status(200).send(block);
 | 
			
		||||
@ -217,18 +213,18 @@ class BitcoinBackendRoutes {
 | 
			
		||||
    const blockHeight = req.query.height;
 | 
			
		||||
    try {
 | 
			
		||||
      if (typeof(blockHeight) !== 'string') {
 | 
			
		||||
        res.status(400).send(`invalid param blockHeight, must be a string representing an integer`);
 | 
			
		||||
        res.status(400).send(`invalid param blockHeight ${blockHeight}, must be a string representing an integer`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const blockHeightNumber = parseInt(blockHeight, 10);
 | 
			
		||||
      if (typeof(blockHeightNumber) !== 'number') {
 | 
			
		||||
        res.status(400).send(`invalid param blockHeight. must be a valid integer`);
 | 
			
		||||
        res.status(400).send(`invalid param blockHeight ${blockHeight}. must be a valid integer`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const block = await bitcoinClient.getBlockHash(blockHeightNumber);
 | 
			
		||||
      if (!block) {
 | 
			
		||||
        res.status(400).send(`unable to get block hash`);
 | 
			
		||||
        res.status(400).send(`unable to get block hash for block height ${blockHeightNumber}`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      res.status(200).send(block);
 | 
			
		||||
@ -251,4 +247,4 @@ class BitcoinBackendRoutes {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new BitcoinBackendRoutes;
 | 
			
		||||
export default new BitcoinBackendRoutes
 | 
			
		||||
@ -21,12 +21,6 @@ import transactionRepository from '../../repositories/TransactionRepository';
 | 
			
		||||
import rbfCache from '../rbf-cache';
 | 
			
		||||
import { calculateMempoolTxCpfp } from '../cpfp';
 | 
			
		||||
import { handleError } from '../../utils/api';
 | 
			
		||||
import poolsUpdater from '../../tasks/pools-updater';
 | 
			
		||||
 | 
			
		||||
const TXID_REGEX = /^[a-f0-9]{64}$/i;
 | 
			
		||||
const BLOCK_HASH_REGEX = /^[a-f0-9]{64}$/i;
 | 
			
		||||
const ADDRESS_REGEX = /^[a-z0-9]{2,120}$/i;
 | 
			
		||||
const SCRIPT_HASH_REGEX = /^([a-f0-9]{2})+$/i;
 | 
			
		||||
 | 
			
		||||
class BitcoinRoutes {
 | 
			
		||||
  public initRoutes(app: Application) {
 | 
			
		||||
@ -57,10 +51,6 @@ class BitcoinRoutes {
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this))
 | 
			
		||||
      // Temporarily add txs/package endpoint for all backends until esplora supports it
 | 
			
		||||
      .post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage)
 | 
			
		||||
      // Internal routes
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'internal/blocks/definition/list', this.getBlockDefinitionHashes)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'internal/blocks/definition/current', this.getCurrentBlockDefinitionHash)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'internal/blocks/:definitionHash', this.getBlocksByDefinitionHash)
 | 
			
		||||
      ;
 | 
			
		||||
 | 
			
		||||
      if (config.MEMPOOL.BACKEND !== 'esplora') {
 | 
			
		||||
@ -100,7 +90,7 @@ class BitcoinRoutes {
 | 
			
		||||
      res.set('Content-Type', 'application/json');
 | 
			
		||||
      res.send(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get init data');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -119,7 +109,7 @@ class BitcoinRoutes {
 | 
			
		||||
      const result = mempoolBlocks.getMempoolBlocks();
 | 
			
		||||
      res.json(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get mempool blocks');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -131,10 +121,7 @@ class BitcoinRoutes {
 | 
			
		||||
    const txIds: string[] = [];
 | 
			
		||||
    for (const _txId in req.query.txId) {
 | 
			
		||||
      if (typeof req.query.txId[_txId] === 'string') {
 | 
			
		||||
        const txid = req.query.txId[_txId].toString();
 | 
			
		||||
        if (TXID_REGEX.test(txid)) {
 | 
			
		||||
          txIds.push(txid);
 | 
			
		||||
        }
 | 
			
		||||
        txIds.push(req.query.txId[_txId].toString());
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -153,22 +140,18 @@ class BitcoinRoutes {
 | 
			
		||||
      handleError(req, res, 400, 'Too many txids requested');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (txids.some((txid) => !TXID_REGEX.test(txid))) {
 | 
			
		||||
      handleError(req, res, 400, 'Invalid txids format');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
 | 
			
		||||
      res.json(batchedOutspends);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get batched outspends');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getCpfpInfo(req: Request, res: Response) {
 | 
			
		||||
    if (!TXID_REGEX.test(req.params.txId)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid transaction ID`);
 | 
			
		||||
    if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid transaction ID.`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -201,7 +184,7 @@ class BitcoinRoutes {
 | 
			
		||||
        try {
 | 
			
		||||
          cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          handleError(req, res, 500, 'Failed to get CPFP info');
 | 
			
		||||
          handleError(req, res, 500, 'failed to get CPFP info');
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
@ -222,10 +205,6 @@ class BitcoinRoutes {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getTransaction(req: Request, res: Response) {
 | 
			
		||||
    if (!TXID_REGEX.test(req.params.txId)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid transaction ID`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true, false, false, true);
 | 
			
		||||
      res.json(transaction);
 | 
			
		||||
@ -233,18 +212,12 @@ class BitcoinRoutes {
 | 
			
		||||
      let statusCode = 500;
 | 
			
		||||
      if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
 | 
			
		||||
        statusCode = 404;
 | 
			
		||||
        handleError(req, res, statusCode, 'No such mempool or blockchain transaction');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      handleError(req, res, statusCode, 'Failed to get transaction');
 | 
			
		||||
      handleError(req, res, statusCode, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getRawTransaction(req: Request, res: Response) {
 | 
			
		||||
    if (!TXID_REGEX.test(req.params.txId)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid transaction ID`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(req.params.txId, true);
 | 
			
		||||
      res.setHeader('content-type', 'text/plain');
 | 
			
		||||
@ -253,10 +226,8 @@ class BitcoinRoutes {
 | 
			
		||||
      let statusCode = 500;
 | 
			
		||||
      if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
 | 
			
		||||
        statusCode = 404;
 | 
			
		||||
        handleError(req, res, statusCode, 'No such mempool or blockchain transaction');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      handleError(req, res, statusCode, 'Failed to get raw transaction');
 | 
			
		||||
      handleError(req, res, statusCode, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -321,18 +292,14 @@ class BitcoinRoutes {
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      if (e instanceof Error && new RegExp(notFoundError).test(e.message)) {
 | 
			
		||||
        handleError(req, res, 404, notFoundError);
 | 
			
		||||
        handleError(req, res, 404, e.message);
 | 
			
		||||
      } else {
 | 
			
		||||
        handleError(req, res, 500, 'Failed to process PSBT');
 | 
			
		||||
        handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getTransactionStatus(req: Request, res: Response) {
 | 
			
		||||
    if (!TXID_REGEX.test(req.params.txId)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid transaction ID`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
 | 
			
		||||
      res.json(transaction.status);
 | 
			
		||||
@ -340,54 +307,36 @@ class BitcoinRoutes {
 | 
			
		||||
      let statusCode = 500;
 | 
			
		||||
      if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
 | 
			
		||||
        statusCode = 404;
 | 
			
		||||
        handleError(req, res, statusCode, 'No such mempool or blockchain transaction');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      handleError(req, res, statusCode, 'Failed to get transaction status');
 | 
			
		||||
      handleError(req, res, statusCode, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getStrippedBlockTransactions(req: Request, res: Response) {
 | 
			
		||||
    if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid block hash`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash);
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
 | 
			
		||||
      res.json(transactions);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get block summary');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getStrippedBlockTransaction(req: Request, res: Response) {
 | 
			
		||||
    if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid block hash`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!TXID_REGEX.test(req.params.txid)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid transaction ID`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const transaction = await blocks.$getSingleTxFromSummary(req.params.hash, req.params.txid);
 | 
			
		||||
      if (!transaction) {
 | 
			
		||||
        handleError(req, res, 404, `Transaction not found in summary`);
 | 
			
		||||
        handleError(req, res, 404, `transaction not found in summary`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
 | 
			
		||||
      res.json(transaction);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get transaction from summary');
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getBlock(req: Request, res: Response) {
 | 
			
		||||
    if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid block hash`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const block = await blocks.$getBlock(req.params.hash);
 | 
			
		||||
 | 
			
		||||
@ -399,69 +348,53 @@ class BitcoinRoutes {
 | 
			
		||||
      } else if (blockAge > 30 * day) {
 | 
			
		||||
        cacheDuration = 10 * day;
 | 
			
		||||
      } else {
 | 
			
		||||
        cacheDuration = 600;
 | 
			
		||||
        cacheDuration = 600
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
 | 
			
		||||
      res.json(block);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get block');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getBlockHeader(req: Request, res: Response) {
 | 
			
		||||
    if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid block hash`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const blockHeader = await bitcoinApi.$getBlockHeader(req.params.hash);
 | 
			
		||||
      res.setHeader('content-type', 'text/plain');
 | 
			
		||||
      res.send(blockHeader);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get block header');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getBlockAuditSummary(req: Request, res: Response) {
 | 
			
		||||
    if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid block hash`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const auditSummary = await blocks.$getBlockAuditSummary(req.params.hash);
 | 
			
		||||
      if (auditSummary) {
 | 
			
		||||
        res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
 | 
			
		||||
        res.json(auditSummary);
 | 
			
		||||
      } else {
 | 
			
		||||
        handleError(req, res, 404, `Audit not available`);
 | 
			
		||||
        handleError(req, res, 404, `audit not available`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get block audit summary');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getBlockTxAuditSummary(req: Request, res: Response) {
 | 
			
		||||
    if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid block hash`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!TXID_REGEX.test(req.params.txid)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid transaction ID`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid);
 | 
			
		||||
      if (auditSummary) {
 | 
			
		||||
        res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
 | 
			
		||||
        res.json(auditSummary);
 | 
			
		||||
      } else {
 | 
			
		||||
        handleError(req, res, 404, `Transaction audit not available`);
 | 
			
		||||
        handleError(req, res, 404, `transaction audit not available`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get transaction audit summary');
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -475,7 +408,7 @@ class BitcoinRoutes {
 | 
			
		||||
        return await this.getLegacyBlocks(req, res);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get blocks');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -517,7 +450,7 @@ class BitcoinRoutes {
 | 
			
		||||
      res.json(await blocks.$getBlocksBetweenHeight(from, to));
 | 
			
		||||
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get blocks');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -552,15 +485,11 @@ class BitcoinRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(returnBlocks);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get blocks');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getBlockTransactions(req: Request, res: Response) {
 | 
			
		||||
    if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid block hash`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
 | 
			
		||||
 | 
			
		||||
@ -581,7 +510,7 @@ class BitcoinRoutes {
 | 
			
		||||
      res.json(transactions);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get block transactions');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -590,7 +519,7 @@ class BitcoinRoutes {
 | 
			
		||||
      const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
 | 
			
		||||
      res.send(blockHash);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get block at height');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -599,20 +528,16 @@ class BitcoinRoutes {
 | 
			
		||||
      handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!ADDRESS_REGEX.test(req.params.address)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid address`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const addressData = await bitcoinApi.$getAddress(req.params.address);
 | 
			
		||||
      res.json(addressData);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
 | 
			
		||||
        handleError(req, res, 413, e.message);
 | 
			
		||||
        handleError(req, res, 413, e instanceof Error ? e.message : e);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get address');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -621,10 +546,6 @@ class BitcoinRoutes {
 | 
			
		||||
      handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!ADDRESS_REGEX.test(req.params.address)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid address`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      let lastTxId: string = '';
 | 
			
		||||
@ -635,10 +556,10 @@ class BitcoinRoutes {
 | 
			
		||||
      res.json(transactions);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
 | 
			
		||||
        handleError(req, res, 413, e.message);
 | 
			
		||||
        handleError(req, res, 413, e instanceof Error ? e.message : e);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get address transactions');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -654,10 +575,6 @@ class BitcoinRoutes {
 | 
			
		||||
      handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!SCRIPT_HASH_REGEX.test(req.params.scripthash)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid scripthash`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      // electrum expects scripthashes in little-endian
 | 
			
		||||
@ -666,10 +583,10 @@ class BitcoinRoutes {
 | 
			
		||||
      res.json(addressData);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
 | 
			
		||||
        handleError(req, res, 413, e.message);
 | 
			
		||||
        handleError(req, res, 413, e instanceof Error ? e.message : e);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get script hash');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -678,10 +595,6 @@ class BitcoinRoutes {
 | 
			
		||||
      handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!SCRIPT_HASH_REGEX.test(req.params.scripthash)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid scripthash`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      // electrum expects scripthashes in little-endian
 | 
			
		||||
@ -694,10 +607,10 @@ class BitcoinRoutes {
 | 
			
		||||
      res.json(transactions);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
 | 
			
		||||
        handleError(req, res, 413, e.message);
 | 
			
		||||
        handleError(req, res, 413, e instanceof Error ? e.message : e);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get script hash transactions');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -710,10 +623,10 @@ class BitcoinRoutes {
 | 
			
		||||
 | 
			
		||||
  private async getAddressPrefix(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const addressPrefix = await bitcoinApi.$getAddressPrefix(req.params.prefix);
 | 
			
		||||
      res.send(addressPrefix);
 | 
			
		||||
      const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
 | 
			
		||||
      res.send(blockHash);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get address prefix');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -744,52 +657,6 @@ class BitcoinRoutes {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getBlockDefinitionHashes(req: Request, res: Response): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await blocks.$getBlockDefinitionHashes();
 | 
			
		||||
      if (!result) {
 | 
			
		||||
        handleError(req, res, 503, `Service Temporarily Unavailable`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      res.setHeader('content-type', 'application/json');
 | 
			
		||||
      res.send(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getCurrentBlockDefinitionHash(req: Request, res: Response): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const currentSha = await poolsUpdater.getShaFromDb();
 | 
			
		||||
      if (!currentSha) {
 | 
			
		||||
        handleError(req, res, 503, `Service Temporarily Unavailable`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      res.setHeader('content-type', 'text/plain');
 | 
			
		||||
      res.send(currentSha);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getBlocksByDefinitionHash(req: Request, res: Response): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      if (typeof(req.params.definitionHash) !== 'string') {
 | 
			
		||||
        res.status(400).send('Parameter "hash" must be a valid string');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const blocksHash = await blocks.$getBlocksByDefinitionHash(req.params.definitionHash as string);
 | 
			
		||||
      if (!blocksHash) {
 | 
			
		||||
        handleError(req, res, 503, `Service Temporarily Unavailable`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      res.setHeader('content-type', 'application/json');
 | 
			
		||||
      res.send(blocksHash);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getBlockTipHeight(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = blocks.getCurrentBlockHeight();
 | 
			
		||||
@ -800,7 +667,7 @@ class BitcoinRoutes {
 | 
			
		||||
      res.setHeader('content-type', 'text/plain');
 | 
			
		||||
      res.send(result.toString());
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get height at tip');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -810,55 +677,39 @@ class BitcoinRoutes {
 | 
			
		||||
      res.setHeader('content-type', 'text/plain');
 | 
			
		||||
      res.send(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get hash at tip');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getRawBlock(req: Request, res: Response) {
 | 
			
		||||
    if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid block hash`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await bitcoinApi.$getRawBlock(req.params.hash);
 | 
			
		||||
      res.setHeader('content-type', 'application/octet-stream');
 | 
			
		||||
      res.send(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get raw block');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getTxIdsForBlock(req: Request, res: Response) {
 | 
			
		||||
    if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid block hash`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
 | 
			
		||||
      res.json(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get txids for block');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async validateAddress(req: Request, res: Response) {
 | 
			
		||||
    if (!ADDRESS_REGEX.test(req.params.address)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid address`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await bitcoinClient.validateAddress(req.params.address);
 | 
			
		||||
      res.json(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to validate address');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getRbfHistory(req: Request, res: Response) {
 | 
			
		||||
    if (!TXID_REGEX.test(req.params.txId)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid transaction ID`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const replacements = rbfCache.getRbfTree(req.params.txId) || null;
 | 
			
		||||
      const replaces = rbfCache.getReplaces(req.params.txId) || null;
 | 
			
		||||
@ -867,7 +718,7 @@ class BitcoinRoutes {
 | 
			
		||||
        replaces
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get rbf history');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -876,7 +727,7 @@ class BitcoinRoutes {
 | 
			
		||||
      const result = rbfCache.getRbfTrees(false);
 | 
			
		||||
      res.json(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get rbf trees');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -885,15 +736,11 @@ class BitcoinRoutes {
 | 
			
		||||
      const result = rbfCache.getRbfTrees(true);
 | 
			
		||||
      res.json(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get full rbf replacements');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getCachedTx(req: Request, res: Response) {
 | 
			
		||||
    if (!TXID_REGEX.test(req.params.txId)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid transaction ID`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const result = rbfCache.getTx(req.params.txId);
 | 
			
		||||
      if (result) {
 | 
			
		||||
@ -902,20 +749,16 @@ class BitcoinRoutes {
 | 
			
		||||
        res.status(204).send();
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get cached tx');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getTransactionOutspends(req: Request, res: Response) {
 | 
			
		||||
    if (!TXID_REGEX.test(req.params.txId)) {
 | 
			
		||||
      handleError(req, res, 501, `Invalid transaction ID`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await bitcoinApi.$getOutspends(req.params.txId);
 | 
			
		||||
      res.json(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get transaction outspends');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -928,7 +771,7 @@ class BitcoinRoutes {
 | 
			
		||||
        handleError(req, res, 503, `Service Temporarily Unavailable`);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get difficulty change');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -939,8 +782,8 @@ class BitcoinRoutes {
 | 
			
		||||
      const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
 | 
			
		||||
      res.send(txIdResult);
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code })
 | 
			
		||||
        : 'Failed to send raw transaction');
 | 
			
		||||
      handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
 | 
			
		||||
        : (e.message || 'Error'));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -951,8 +794,8 @@ class BitcoinRoutes {
 | 
			
		||||
      const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
 | 
			
		||||
      res.send(txIdResult);
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code })
 | 
			
		||||
        : 'Failed to send raw transaction');
 | 
			
		||||
      handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
 | 
			
		||||
        : (e.message || 'Error'));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -963,8 +806,8 @@ class BitcoinRoutes {
 | 
			
		||||
      const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
 | 
			
		||||
      res.send(result);
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      handleError(req, res, 400, (e.message && e.code) ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code })
 | 
			
		||||
        : 'Failed to test transactions');
 | 
			
		||||
      handleError(req, res, 400, e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
 | 
			
		||||
        : (e.message || 'Error'));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -976,8 +819,8 @@ class BitcoinRoutes {
 | 
			
		||||
      const result = await bitcoinClient.submitPackage(rawTxs, maxfeerate ?? undefined, maxburnamount ?? undefined);
 | 
			
		||||
      res.send(result);
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      handleError(req, res, 400, (e.message && e.code) ? 'submitpackage RPC error: ' + JSON.stringify({ code: e.code })
 | 
			
		||||
        : 'Failed to submit package');
 | 
			
		||||
      handleError(req, res, 400, e.message && e.code ? 'submitpackage RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
 | 
			
		||||
        : (e.message || 'Error'));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,12 @@
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import axios, { isAxiosError } from 'axios';
 | 
			
		||||
import axios, { AxiosResponse, isAxiosError } from 'axios';
 | 
			
		||||
import http from 'http';
 | 
			
		||||
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
 | 
			
		||||
import { IEsploraApi } from './esplora-api.interface';
 | 
			
		||||
import logger from '../../logger';
 | 
			
		||||
import { Common } from '../common';
 | 
			
		||||
import { SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
 | 
			
		||||
import os from 'os';
 | 
			
		||||
 | 
			
		||||
interface FailoverHost {
 | 
			
		||||
  host: string,
 | 
			
		||||
  rtts: number[],
 | 
			
		||||
@ -20,13 +20,6 @@ interface FailoverHost {
 | 
			
		||||
  preferred?: boolean,
 | 
			
		||||
  checked: boolean,
 | 
			
		||||
  lastChecked?: number,
 | 
			
		||||
  publicDomain: string,
 | 
			
		||||
  hashes: {
 | 
			
		||||
    frontend?: string,
 | 
			
		||||
    backend?: string,
 | 
			
		||||
    electrs?: string,
 | 
			
		||||
    lastUpdated: number,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class FailoverRouter {
 | 
			
		||||
@ -36,21 +29,14 @@ class FailoverRouter {
 | 
			
		||||
  maxHeight: number = 0;
 | 
			
		||||
  hosts: FailoverHost[];
 | 
			
		||||
  multihost: boolean;
 | 
			
		||||
  gitHashInterval: number = 600000; // 10 minutes
 | 
			
		||||
  pollInterval: number = 60000; // 1 minute
 | 
			
		||||
  pollInterval: number = 60000;
 | 
			
		||||
  pollTimer: NodeJS.Timeout | null = null;
 | 
			
		||||
  pollConnection = axios.create();
 | 
			
		||||
  localHostname: string = 'localhost';
 | 
			
		||||
  requestConnection = axios.create({
 | 
			
		||||
    httpAgent: new http.Agent({ keepAlive: true })
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    try {
 | 
			
		||||
      this.localHostname = os.hostname();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.warn('Failed to set local hostname, using "localhost"');
 | 
			
		||||
    }
 | 
			
		||||
    // setup list of hosts
 | 
			
		||||
    this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => {
 | 
			
		||||
      return {
 | 
			
		||||
@ -59,10 +45,6 @@ class FailoverRouter {
 | 
			
		||||
        rtts: [],
 | 
			
		||||
        rtt: Infinity,
 | 
			
		||||
        failures: 0,
 | 
			
		||||
        publicDomain: 'https://' + this.extractPublicDomain(domain),
 | 
			
		||||
        hashes: {
 | 
			
		||||
          lastUpdated: 0,
 | 
			
		||||
        },
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
    this.activeHost = {
 | 
			
		||||
@ -73,10 +55,6 @@ class FailoverRouter {
 | 
			
		||||
      socket: !!config.ESPLORA.UNIX_SOCKET_PATH,
 | 
			
		||||
      preferred: true,
 | 
			
		||||
      checked: false,
 | 
			
		||||
      publicDomain: `http://${this.localHostname}`,
 | 
			
		||||
      hashes: {
 | 
			
		||||
        lastUpdated: 0,
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
    this.fallbackHost = this.activeHost;
 | 
			
		||||
    this.hosts.unshift(this.activeHost);
 | 
			
		||||
@ -128,24 +106,6 @@ class FailoverRouter {
 | 
			
		||||
            host.outOfSync = false;
 | 
			
		||||
          }
 | 
			
		||||
          host.unreachable = false;
 | 
			
		||||
 | 
			
		||||
          // update esplora git hash using the x-powered-by header from the height check
 | 
			
		||||
          const poweredBy = result.headers['x-powered-by'];
 | 
			
		||||
          if (poweredBy) {
 | 
			
		||||
            const match = poweredBy.match(/([a-fA-F0-9]{5,40})/);
 | 
			
		||||
            if (match && match[1]?.length) {
 | 
			
		||||
              host.hashes.electrs = match[1];
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // Check front and backend git hashes less often
 | 
			
		||||
          if (Date.now() - host.hashes.lastUpdated > this.gitHashInterval) {
 | 
			
		||||
            await Promise.all([
 | 
			
		||||
              this.$updateFrontendGitHash(host),
 | 
			
		||||
              this.$updateBackendGitHash(host)
 | 
			
		||||
            ]);
 | 
			
		||||
            host.hashes.lastUpdated = Date.now();
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          host.outOfSync = true;
 | 
			
		||||
          host.unreachable = true;
 | 
			
		||||
@ -242,47 +202,6 @@ class FailoverRouter {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // methods for retrieving git hashes by host
 | 
			
		||||
  private async $updateFrontendGitHash(host: FailoverHost): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const url = `${host.publicDomain}/resources/config.js`;
 | 
			
		||||
      const response = await this.pollConnection.get<string>(url, { timeout: config.ESPLORA.FALLBACK_TIMEOUT });
 | 
			
		||||
      const match = response.data.match(/GIT_COMMIT_HASH\s*=\s*['"](.*?)['"]/);
 | 
			
		||||
      if (match && match[1]?.length) {
 | 
			
		||||
        host.hashes.frontend = match[1];
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      // failed to get frontend build hash - do nothing
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $updateBackendGitHash(host: FailoverHost): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const url = `${host.publicDomain}/api/v1/backend-info`;
 | 
			
		||||
      const response = await this.pollConnection.get<any>(url, { timeout: config.ESPLORA.FALLBACK_TIMEOUT });
 | 
			
		||||
      if (response.data?.gitCommit) {
 | 
			
		||||
        host.hashes.backend = response.data.gitCommit;
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      // failed to get backend build hash - do nothing
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // returns the public mempool domain corresponding to an esplora server url
 | 
			
		||||
  // (a bit of a hack to avoid manually specifying frontend & backend URLs for each esplora server)
 | 
			
		||||
  private extractPublicDomain(url: string): string {
 | 
			
		||||
    // force the url to start with a valid protocol
 | 
			
		||||
    const urlWithProtocol = url.startsWith('http') ? url : `https://${url}`;
 | 
			
		||||
    // parse as URL and extract the hostname
 | 
			
		||||
    try {
 | 
			
		||||
      const parsed = new URL(urlWithProtocol);
 | 
			
		||||
      return parsed.hostname;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      // fallback to the original url
 | 
			
		||||
      return url;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $query<T>(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true): Promise<T> {
 | 
			
		||||
    let axiosConfig;
 | 
			
		||||
    let url;
 | 
			
		||||
@ -462,7 +381,6 @@ class ElectrsApi implements AbstractBitcoinApi {
 | 
			
		||||
        unreachable: !!host.unreachable,
 | 
			
		||||
        checked: !!host.checked,
 | 
			
		||||
        lastChecked: host.lastChecked || 0,
 | 
			
		||||
        hashes: host.hashes,
 | 
			
		||||
      }));
 | 
			
		||||
    } else {
 | 
			
		||||
      return [];
 | 
			
		||||
 | 
			
		||||
@ -33,8 +33,8 @@ import AccelerationRepository from '../repositories/AccelerationRepository';
 | 
			
		||||
import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
 | 
			
		||||
import mempool from './mempool';
 | 
			
		||||
import CpfpRepository from '../repositories/CpfpRepository';
 | 
			
		||||
import accelerationApi from './services/acceleration';
 | 
			
		||||
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
 | 
			
		||||
import database from '../database';
 | 
			
		||||
 | 
			
		||||
class Blocks {
 | 
			
		||||
  private blocks: BlockExtended[] = [];
 | 
			
		||||
@ -1462,36 +1462,6 @@ class Blocks {
 | 
			
		||||
      // not a fatal error, we'll try again next time the indexer runs
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getBlockDefinitionHashes(): Promise<string[] | null> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any = await database.query(`SELECT DISTINCT(definition_hash) FROM blocks`);
 | 
			
		||||
      if (rows && Array.isArray(rows)) {
 | 
			
		||||
        return rows.map(r => r.definition_hash);
 | 
			
		||||
      } else {
 | 
			
		||||
        logger.debug(`Unable to retreive list of blocks.definition_hash from db (no result)`);
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.debug(`Unable to retreive list of blocks.definition_hash from db (exception: ${e})`);
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getBlocksByDefinitionHash(definitionHash: string): Promise<string[] | null> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any = await database.query(`SELECT hash FROM blocks WHERE definition_hash = ?`, [definitionHash]);
 | 
			
		||||
      if (rows && Array.isArray(rows)) {
 | 
			
		||||
        return rows.map(r => r.hash);
 | 
			
		||||
      } else {
 | 
			
		||||
        logger.debug(`Unable to retreive list of blocks for definition hash ${definitionHash} from db (no result)`);
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.debug(`Unable to retreive list of blocks for definition hash ${definitionHash} from db (exception: ${e})`);
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new Blocks();
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
 | 
			
		||||
import { RowDataPacket } from 'mysql2';
 | 
			
		||||
 | 
			
		||||
class DatabaseMigration {
 | 
			
		||||
  private static currentVersion = 95;
 | 
			
		||||
  private static currentVersion = 93;
 | 
			
		||||
  private queryTimeout = 3600_000;
 | 
			
		||||
  private statisticsAddedIndexed = false;
 | 
			
		||||
  private uniqueLogs: string[] = [];
 | 
			
		||||
@ -801,335 +801,6 @@ class DatabaseMigration {
 | 
			
		||||
      `);
 | 
			
		||||
      await this.updateToSchemaVersion(93);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Unify database schema for all mempool netwoks
 | 
			
		||||
    // versions above 94 should not use network-specific flags
 | 
			
		||||
    if (databaseSchemaVersion < 94) {
 | 
			
		||||
 | 
			
		||||
      if (!isBitcoin) {
 | 
			
		||||
        // Apply all the bitcoin specific migrations to non-bitcoin networks: liquid, liquidtestnet and testnet4 (!)
 | 
			
		||||
        // Version 5
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
 | 
			
		||||
 | 
			
		||||
        // Version 6
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks MODIFY `tx_count` smallint unsigned NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks MODIFY `size` integer unsigned NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks MODIFY `weight` integer unsigned NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` double NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks DROP FOREIGN KEY IF EXISTS `blocks_ibfk_1`');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE pools MODIFY `id` smallint unsigned AUTO_INCREMENT');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks MODIFY `pool_id` smallint unsigned NULL');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks ADD FOREIGN KEY (`pool_id`) REFERENCES `pools` (`id`)');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks ADD `version` integer unsigned NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks ADD `bits` integer unsigned NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks ADD `nonce` bigint unsigned NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL');
 | 
			
		||||
 | 
			
		||||
        // Version 7
 | 
			
		||||
        await this.$executeQuery('DROP table IF EXISTS hashrates;');
 | 
			
		||||
        await this.$executeQuery(this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates'));
 | 
			
		||||
 | 
			
		||||
        // Version 8
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `hashrates` DROP INDEX `PRIMARY`');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"');
 | 
			
		||||
 | 
			
		||||
        // Version 9
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `state` CHANGE `name` `name` varchar(100)');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)');
 | 
			
		||||
 | 
			
		||||
        // Version 10
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `blockTimestamp` (`blockTimestamp`)');
 | 
			
		||||
 | 
			
		||||
        // Version 11
 | 
			
		||||
        await this.$executeQuery(`ALTER TABLE blocks
 | 
			
		||||
          ADD avg_fee INT UNSIGNED NULL,
 | 
			
		||||
          ADD avg_fee_rate INT UNSIGNED NULL
 | 
			
		||||
        `);
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` INT UNSIGNED NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` INT UNSIGNED NOT NULL DEFAULT "0"');
 | 
			
		||||
 | 
			
		||||
        // Version 12
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
 | 
			
		||||
 | 
			
		||||
        // Version 13
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` DOUBLE UNSIGNED NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee_rate` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
 | 
			
		||||
 | 
			
		||||
        // Version 14
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `hashrates` DROP FOREIGN KEY `hashrates_ibfk_1`');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `hashrates` MODIFY `pool_id` SMALLINT UNSIGNED NOT NULL DEFAULT "0"');
 | 
			
		||||
 | 
			
		||||
        // Version 17
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `pools` ADD `slug` CHAR(50) NULL');
 | 
			
		||||
 | 
			
		||||
        // Version 18
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `hash` (`hash`);');
 | 
			
		||||
 | 
			
		||||
        // Version 20
 | 
			
		||||
        await this.$executeQuery(this.getCreateBlocksSummariesTableQuery(), await this.$checkIfTableExists('blocks_summaries'));
 | 
			
		||||
 | 
			
		||||
        // Version 22
 | 
			
		||||
        await this.$executeQuery('DROP TABLE IF EXISTS `difficulty_adjustments`');
 | 
			
		||||
        await this.$executeQuery(this.getCreateDifficultyAdjustmentsTableQuery(), await this.$checkIfTableExists('difficulty_adjustments'));
 | 
			
		||||
 | 
			
		||||
        // Version 24
 | 
			
		||||
        await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`');
 | 
			
		||||
        await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits'));
 | 
			
		||||
 | 
			
		||||
        // Version 25
 | 
			
		||||
        await this.$executeQuery(this.getCreateLightningStatisticsQuery(), await this.$checkIfTableExists('lightning_stats'));
 | 
			
		||||
        await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes'));
 | 
			
		||||
        await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels'));
 | 
			
		||||
        await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats'));
 | 
			
		||||
 | 
			
		||||
        // Version 26
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_nodes int(11) NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `lightning_stats` ADD unannounced_nodes int(11) NOT NULL DEFAULT "0"');
 | 
			
		||||
 | 
			
		||||
        // Version 27
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
 | 
			
		||||
 | 
			
		||||
        // Version 28
 | 
			
		||||
        await this.$executeQuery(`ALTER TABLE lightning_stats MODIFY added DATE`);
 | 
			
		||||
 | 
			
		||||
        // Version 29
 | 
			
		||||
        await this.$executeQuery(this.getCreateGeoNamesTableQuery(), await this.$checkIfTableExists('geo_names'));
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `nodes` ADD as_number int(11) unsigned NULL DEFAULT NULL');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `nodes` ADD city_id int(11) unsigned NULL DEFAULT NULL');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `nodes` ADD country_id int(11) unsigned NULL DEFAULT NULL');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `nodes` ADD accuracy_radius int(11) unsigned NULL DEFAULT NULL');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `nodes` ADD subdivision_id int(11) unsigned NULL DEFAULT NULL');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `nodes` ADD longitude double NULL DEFAULT NULL');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `nodes` ADD latitude double NULL DEFAULT NULL');
 | 
			
		||||
 | 
			
		||||
        // Version 30
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization") NOT NULL');
 | 
			
		||||
 | 
			
		||||
        // Version 31
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `prices` ADD `id` int NULL AUTO_INCREMENT UNIQUE');
 | 
			
		||||
        await this.$executeQuery('DROP TABLE IF EXISTS `blocks_prices`');
 | 
			
		||||
        await this.$executeQuery(this.getCreateBlocksPricesTableQuery(), await this.$checkIfTableExists('blocks_prices'));
 | 
			
		||||
 | 
			
		||||
        // Version 32
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD `template` JSON DEFAULT "[]"');
 | 
			
		||||
 | 
			
		||||
        // Version 33
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization", "country_iso_code") NOT NULL');
 | 
			
		||||
 | 
			
		||||
        // Version 34
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_tor_nodes int(11) NOT NULL DEFAULT "0"');
 | 
			
		||||
    
 | 
			
		||||
        // Version 35
 | 
			
		||||
        await this.$executeQuery('DELETE from `lightning_stats` WHERE added > "2021-09-19"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `lightning_stats` ADD CONSTRAINT added_unique UNIQUE (added);');
 | 
			
		||||
 | 
			
		||||
        // Version 36
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `nodes` ADD status TINYINT NOT NULL DEFAULT "1"');
 | 
			
		||||
    
 | 
			
		||||
        // Version 37
 | 
			
		||||
        await this.$executeQuery(this.getCreateLNNodesSocketsTableQuery(), await this.$checkIfTableExists('nodes_sockets'));
 | 
			
		||||
        
 | 
			
		||||
        // Version 38
 | 
			
		||||
        await this.$executeQuery(`TRUNCATE lightning_stats`);
 | 
			
		||||
        await this.$executeQuery(`TRUNCATE node_stats`);
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `lightning_stats` CHANGE `added` `added` timestamp NULL');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `node_stats` CHANGE `added` `added` timestamp NULL');
 | 
			
		||||
        await this.updateToSchemaVersion(38);
 | 
			
		||||
      
 | 
			
		||||
        // Version 39
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `nodes` ADD alias_search TEXT NULL DEFAULT NULL AFTER `alias`');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE nodes ADD FULLTEXT(alias_search)');
 | 
			
		||||
 | 
			
		||||
        // Version 40
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `nodes` ADD capacity bigint(20) unsigned DEFAULT NULL');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `nodes` ADD channels int(11) unsigned DEFAULT NULL');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `nodes` ADD INDEX `capacity` (`capacity`);');
 | 
			
		||||
 | 
			
		||||
        // Version 41
 | 
			
		||||
        await this.$executeQuery('UPDATE channels SET closing_reason = NULL WHERE closing_reason = 1');
 | 
			
		||||
 | 
			
		||||
        // Version 42
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `channels` ADD closing_resolved tinyint(1) DEFAULT 0');
 | 
			
		||||
      
 | 
			
		||||
        // Version 43
 | 
			
		||||
        await this.$executeQuery(this.getCreateLNNodeRecordsTableQuery(), await this.$checkIfTableExists('nodes_records'));
 | 
			
		||||
 | 
			
		||||
        // Version 44
 | 
			
		||||
        await this.$executeQuery('UPDATE blocks_summaries SET template = NULL');
 | 
			
		||||
 | 
			
		||||
        // Version 45
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fresh_txs JSON DEFAULT "[]"');
 | 
			
		||||
    
 | 
			
		||||
        // Version 48
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `channels` ADD source_checked tinyint(1) DEFAULT 0');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `channels` ADD closing_fee bigint(20) unsigned DEFAULT 0');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `channels` ADD node1_funding_balance bigint(20) unsigned DEFAULT 0');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `channels` ADD node2_funding_balance bigint(20) unsigned DEFAULT 0');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `channels` ADD node1_closing_balance bigint(20) unsigned DEFAULT 0');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `channels` ADD node2_closing_balance bigint(20) unsigned DEFAULT 0');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `channels` ADD funding_ratio float unsigned DEFAULT NULL');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `channels` ADD closed_by varchar(66) DEFAULT NULL');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `channels` ADD single_funded tinyint(1) DEFAULT 0');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `channels` ADD outputs JSON DEFAULT "[]"');
 | 
			
		||||
 | 
			
		||||
        // Version 57
 | 
			
		||||
        await this.$executeQuery(`ALTER TABLE nodes MODIFY updated_at datetime NULL`);
 | 
			
		||||
 | 
			
		||||
        // Version 60
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `blocks_audits` ADD sigop_txs JSON DEFAULT "[]"');
 | 
			
		||||
 | 
			
		||||
        // Version 61
 | 
			
		||||
        if (! await this.$checkIfTableExists('blocks_templates')) {
 | 
			
		||||
          await this.$executeQuery('CREATE TABLE blocks_templates AS SELECT id, template FROM blocks_summaries WHERE template != "[]"');
 | 
			
		||||
        }
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks_templates MODIFY template JSON DEFAULT "[]"');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks_templates ADD PRIMARY KEY (id)');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE blocks_summaries DROP COLUMN template');
 | 
			
		||||
 | 
			
		||||
        // Version 62
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_fees BIGINT UNSIGNED DEFAULT NULL');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_weight BIGINT UNSIGNED DEFAULT NULL');
 | 
			
		||||
      
 | 
			
		||||
        // Version 63
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fullrbf_txs JSON DEFAULT "[]"');
 | 
			
		||||
    
 | 
			
		||||
        // Version 64
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `nodes` ADD features text NULL');
 | 
			
		||||
    
 | 
			
		||||
        // Version 65
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `blocks_audits` ADD accelerated_txs JSON DEFAULT "[]"');
 | 
			
		||||
 | 
			
		||||
        // Version 67
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD version INT NOT NULL DEFAULT 0');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD INDEX `version` (`version`)');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `blocks_templates` ADD version INT NOT NULL DEFAULT 0');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `blocks_templates` ADD INDEX `version` (`version`)');
 | 
			
		||||
 | 
			
		||||
        // Version 76
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `blocks_audits` ADD prioritized_txs JSON DEFAULT "[]"');
 | 
			
		||||
 | 
			
		||||
        // Version 81
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `blocks_audits` ADD version INT NOT NULL DEFAULT 0');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `version` (`version`)');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
 | 
			
		||||
 | 
			
		||||
        // Version 83
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL');
 | 
			
		||||
 | 
			
		||||
        // Version 84
 | 
			
		||||
        await this.$executeQuery(`
 | 
			
		||||
          ALTER TABLE \`pools\`
 | 
			
		||||
            ADD INDEX \`slug\` (\`slug\`),
 | 
			
		||||
            ADD INDEX \`unique_id\` (\`unique_id\`)
 | 
			
		||||
        `);
 | 
			
		||||
 | 
			
		||||
        // Version 85
 | 
			
		||||
        await this.$executeQuery(`
 | 
			
		||||
          ALTER TABLE \`channels\`
 | 
			
		||||
            ADD INDEX \`created\` (\`created\`),
 | 
			
		||||
            ADD INDEX \`capacity\` (\`capacity\`),
 | 
			
		||||
            ADD INDEX \`closing_reason\` (\`closing_reason\`),
 | 
			
		||||
            ADD INDEX \`closing_resolved\` (\`closing_resolved\`)
 | 
			
		||||
        `);
 | 
			
		||||
        
 | 
			
		||||
        // Version 86        
 | 
			
		||||
        await this.$executeQuery(`
 | 
			
		||||
          ALTER TABLE \`nodes\`
 | 
			
		||||
            ADD INDEX \`status\` (\`status\`),
 | 
			
		||||
            ADD INDEX \`channels\` (\`channels\`),
 | 
			
		||||
            ADD INDEX \`country_id\` (\`country_id\`),
 | 
			
		||||
            ADD INDEX \`as_number\` (\`as_number\`),
 | 
			
		||||
            ADD INDEX \`first_seen\` (\`first_seen\`)
 | 
			
		||||
        `);
 | 
			
		||||
 | 
			
		||||
        // Version 87
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `nodes_sockets` ADD INDEX `type` (`type`)');
 | 
			
		||||
        await this.updateToSchemaVersion(87);
 | 
			
		||||
        
 | 
			
		||||
        // Version 88
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `lightning_stats` ADD INDEX `added` (`added`)');
 | 
			
		||||
    
 | 
			
		||||
        // Version 89
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `geo_names` ADD INDEX `names` (`names`)');
 | 
			
		||||
    
 | 
			
		||||
        // Version 90
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `hashrates` ADD INDEX `type` (`type`)');
 | 
			
		||||
 | 
			
		||||
        // Version 91
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)');
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      if (config.MEMPOOL.NETWORK !== 'liquid') {
 | 
			
		||||
        // Apply all the liquid specific migrations to all other networks
 | 
			
		||||
        // Version 68
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE elements_pegs ADD PRIMARY KEY (txid, txindex);');
 | 
			
		||||
        await this.$executeQuery(this.getCreateFederationAddressesTableQuery(), await this.$checkIfTableExists('federation_addresses'));
 | 
			
		||||
        await this.$executeQuery(this.getCreateFederationTxosTableQuery(), await this.$checkIfTableExists('federation_txos'));
 | 
			
		||||
 | 
			
		||||
        // Version 71
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `federation_txos` ADD timelock INT NOT NULL DEFAULT 0');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `federation_txos` ADD expiredAt INT NOT NULL DEFAULT 0');
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `federation_txos` ADD emergencyKey TINYINT NOT NULL DEFAULT 0');
 | 
			
		||||
 | 
			
		||||
        // Version 92
 | 
			
		||||
        await this.$executeQuery(`
 | 
			
		||||
          ALTER TABLE \`elements_pegs\`
 | 
			
		||||
            ADD INDEX \`block\` (\`block\`),
 | 
			
		||||
            ADD INDEX \`datetime\` (\`datetime\`),
 | 
			
		||||
            ADD INDEX \`amount\` (\`amount\`),
 | 
			
		||||
            ADD INDEX \`bitcoinaddress\` (\`bitcoinaddress\`),
 | 
			
		||||
            ADD INDEX \`bitcointxid\` (\`bitcointxid\`)
 | 
			
		||||
        `);
 | 
			
		||||
    
 | 
			
		||||
        // Version 93
 | 
			
		||||
        await this.$executeQuery(`
 | 
			
		||||
          ALTER TABLE \`federation_txos\`
 | 
			
		||||
            ADD INDEX \`unspent\` (\`unspent\`),
 | 
			
		||||
            ADD INDEX \`lastblockupdate\` (\`lastblockupdate\`),
 | 
			
		||||
            ADD INDEX \`blocktime\` (\`blocktime\`),
 | 
			
		||||
            ADD INDEX \`emergencyKey\` (\`emergencyKey\`),
 | 
			
		||||
            ADD INDEX \`expiredAt\` (\`expiredAt\`)
 | 
			
		||||
        `);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (config.MEMPOOL.NETWORK !== 'mainnet') {
 | 
			
		||||
        // Apply all the mainnet specific migrations to all other networks
 | 
			
		||||
        // Version 69
 | 
			
		||||
        await this.$executeQuery(this.getCreateAccelerationsTableQuery(), await this.$checkIfTableExists('accelerations'));
 | 
			
		||||
 | 
			
		||||
        // Version 70
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE accelerations MODIFY COLUMN added DATETIME;');
 | 
			
		||||
 | 
			
		||||
        // Version 77
 | 
			
		||||
        await this.$executeQuery('ALTER TABLE `accelerations` ADD requested datetime DEFAULT NULL');
 | 
			
		||||
      }
 | 
			
		||||
      await this.updateToSchemaVersion(94);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // blocks pools-v2.json hash
 | 
			
		||||
    if (databaseSchemaVersion < 95) {
 | 
			
		||||
      let poolJsonSha = 'f737d86571d190cf1a1a3cf5fd86b33ba9624254';
 | 
			
		||||
      const [poolJsonShaDb]: any[] = await DB.query(`SELECT string FROM state WHERE name = 'pools_json_sha'`);
 | 
			
		||||
      if (poolJsonShaDb?.length > 0) {
 | 
			
		||||
        poolJsonSha = poolJsonShaDb[0].string;
 | 
			
		||||
      }
 | 
			
		||||
      await this.$executeQuery(`ALTER TABLE blocks ADD definition_hash varchar(255) NOT NULL DEFAULT "${poolJsonSha}"`);
 | 
			
		||||
      await this.$executeQuery('ALTER TABLE blocks ADD INDEX `definition_hash` (`definition_hash`)');
 | 
			
		||||
      await this.updateToSchemaVersion(95);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
 | 
			
		||||
@ -3,8 +3,6 @@ import { Application, Request, Response } from 'express';
 | 
			
		||||
import channelsApi from './channels.api';
 | 
			
		||||
import { handleError } from '../../utils/api';
 | 
			
		||||
 | 
			
		||||
const TXID_REGEX = /^[a-f0-9]{64}$/i;
 | 
			
		||||
 | 
			
		||||
class ChannelsRoutes {
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
@ -25,7 +23,7 @@ class ChannelsRoutes {
 | 
			
		||||
      const channels = await channelsApi.$searchChannelsById(req.params.search);
 | 
			
		||||
      res.json(channels);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to search channels by id');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -41,7 +39,7 @@ class ChannelsRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(channel);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get channel');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -72,7 +70,7 @@ class ChannelsRoutes {
 | 
			
		||||
      res.header('X-Total-Count', channelsCount.toString());
 | 
			
		||||
      res.json(channels);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get channels for node');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -85,10 +83,7 @@ class ChannelsRoutes {
 | 
			
		||||
      const txIds: string[] = [];
 | 
			
		||||
      for (const _txId in req.query.txId) {
 | 
			
		||||
        if (typeof req.query.txId[_txId] === 'string') {
 | 
			
		||||
          const txid = req.query.txId[_txId].toString();
 | 
			
		||||
          if (TXID_REGEX.test(txid)) {
 | 
			
		||||
            txIds.push(txid);
 | 
			
		||||
          }
 | 
			
		||||
          txIds.push(req.query.txId[_txId].toString());
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      const channels = await channelsApi.$getChannelsByTransactionId(txIds);
 | 
			
		||||
@ -113,7 +108,7 @@ class ChannelsRoutes {
 | 
			
		||||
 | 
			
		||||
      res.json(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get channels by transaction ids');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -125,7 +120,7 @@ class ChannelsRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(channels);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get penalty closed channels');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -138,7 +133,7 @@ class ChannelsRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(channels);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get channel geodata');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@ class GeneralLightningRoutes {
 | 
			
		||||
        channels: channels,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to search for nodes and channels');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -43,7 +43,7 @@ class GeneralLightningRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(statistics);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get lightning statistics');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -52,7 +52,7 @@ class GeneralLightningRoutes {
 | 
			
		||||
      const statistics = await statisticsApi.$getLatestStatistics();
 | 
			
		||||
      res.json(statistics);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get lightning statistics');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,7 @@ class NodesRoutes {
 | 
			
		||||
      const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search);
 | 
			
		||||
      res.json(nodes);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to search for node');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -188,7 +188,7 @@ class NodesRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(nodes);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get node group');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -204,7 +204,7 @@ class NodesRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(node);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get node');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -216,7 +216,7 @@ class NodesRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(statistics);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get historical node stats');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -232,7 +232,7 @@ class NodesRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(node);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get fee histogram');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -248,7 +248,7 @@ class NodesRoutes {
 | 
			
		||||
        topByChannels: topChannelsNodes,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get nodes ranking');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -260,7 +260,7 @@ class NodesRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(topCapacityNodes);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get top nodes by capacity');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -272,7 +272,7 @@ class NodesRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(topCapacityNodes);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get top nodes by channels');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -284,7 +284,7 @@ class NodesRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(topCapacityNodes);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get oldest nodes');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -296,7 +296,7 @@ class NodesRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
 | 
			
		||||
      res.json(nodesPerAs);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get ISP ranking');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -308,7 +308,7 @@ class NodesRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
 | 
			
		||||
      res.json(worldNodes);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get world nodes');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -336,7 +336,7 @@ class NodesRoutes {
 | 
			
		||||
        nodes: nodes,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get nodes per country');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -363,7 +363,7 @@ class NodesRoutes {
 | 
			
		||||
        nodes: nodes,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get nodes per ISP');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -375,7 +375,7 @@ class NodesRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
 | 
			
		||||
      res.json(nodesPerAs);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get nodes per country');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -83,7 +83,7 @@ class LiquidRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
 | 
			
		||||
      res.json(pegs);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get pegs by month');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -95,7 +95,7 @@ class LiquidRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
 | 
			
		||||
      res.json(reserves);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get reserves by month');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -107,7 +107,7 @@ class LiquidRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
 | 
			
		||||
      res.json(currentSupply);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get pegs');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -119,7 +119,7 @@ class LiquidRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
 | 
			
		||||
      res.json(currentReserves);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get reserves');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -131,7 +131,7 @@ class LiquidRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
 | 
			
		||||
      res.json(auditStatus);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get federation audit status');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -143,7 +143,7 @@ class LiquidRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
 | 
			
		||||
      res.json(federationAddresses);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get federation addresses');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -155,7 +155,7 @@ class LiquidRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
 | 
			
		||||
      res.json(federationAddresses);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get federation addresses');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -167,7 +167,7 @@ class LiquidRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
 | 
			
		||||
      res.json(federationUtxos);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get federation utxos');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -179,7 +179,7 @@ class LiquidRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
 | 
			
		||||
      res.json(expiredUtxos);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get expired utxos');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -191,7 +191,7 @@ class LiquidRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
 | 
			
		||||
      res.json(federationUtxos);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get federation utxos number');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -203,7 +203,7 @@ class LiquidRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
 | 
			
		||||
      res.json(emergencySpentUtxos);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get emergency spent utxos');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -215,7 +215,7 @@ class LiquidRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
 | 
			
		||||
      res.json(emergencySpentUtxos);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get emergency spent utxos stats');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -227,7 +227,7 @@ class LiquidRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
 | 
			
		||||
      res.json(recentPegs);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get pegs list');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -239,7 +239,7 @@ class LiquidRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
 | 
			
		||||
      res.json(pegsVolume);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get pegs volume daily');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -251,7 +251,7 @@ class LiquidRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
 | 
			
		||||
      res.json(pegsCount);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get pegs count');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -72,7 +72,7 @@ class MiningRoutes {
 | 
			
		||||
      }
 | 
			
		||||
      res.status(200).send(response);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get historical prices');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -87,7 +87,7 @@ class MiningRoutes {
 | 
			
		||||
      if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
 | 
			
		||||
        handleError(req, res, 404, e.message);
 | 
			
		||||
      } else {
 | 
			
		||||
        handleError(req, res, 500, 'Failed to get pool');
 | 
			
		||||
        handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -106,7 +106,7 @@ class MiningRoutes {
 | 
			
		||||
      if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
 | 
			
		||||
        handleError(req, res, 404, e.message);
 | 
			
		||||
      } else {
 | 
			
		||||
        handleError(req, res, 500, 'Failed to get blocks for pool');
 | 
			
		||||
        handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -130,7 +130,7 @@ class MiningRoutes {
 | 
			
		||||
        res.json(pools);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get pools');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -144,7 +144,7 @@ class MiningRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(stats);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get pools');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -158,7 +158,7 @@ class MiningRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
 | 
			
		||||
      res.json(hashrates);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get pools historical hashrate');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -175,7 +175,7 @@ class MiningRoutes {
 | 
			
		||||
      if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
 | 
			
		||||
        handleError(req, res, 404, e.message);
 | 
			
		||||
      } else {
 | 
			
		||||
        handleError(req, res, 500, 'Failed to get pool historical hashrate');
 | 
			
		||||
        handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -204,7 +204,7 @@ class MiningRoutes {
 | 
			
		||||
        currentDifficulty: currentDifficulty,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get historical hashrate');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -218,7 +218,7 @@ class MiningRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(blockFees);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get historical block fees');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -236,7 +236,7 @@ class MiningRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(blockFees);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get historical block fees');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -250,7 +250,7 @@ class MiningRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(blockRewards);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get historical block rewards');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -264,7 +264,7 @@ class MiningRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(blockFeeRates);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get historical block fee rates');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -282,7 +282,7 @@ class MiningRoutes {
 | 
			
		||||
        weights: blockWeights
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get historical block size and weight');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -294,7 +294,7 @@ class MiningRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
 | 
			
		||||
      res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get historical difficulty adjustments');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -304,7 +304,7 @@ class MiningRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(response);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get reward stats');
 | 
			
		||||
      res.status(500).end();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -318,7 +318,7 @@ class MiningRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get historical blocks health');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -336,7 +336,7 @@ class MiningRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
 | 
			
		||||
      res.json(audit);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get block audit');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -359,7 +359,7 @@ class MiningRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
 | 
			
		||||
      res.json(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get height from timestamp');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -372,7 +372,7 @@ class MiningRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get block audit scores');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -385,7 +385,7 @@ class MiningRoutes {
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
 | 
			
		||||
      res.json(audit || 'null');
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get block audit score');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -400,7 +400,7 @@ class MiningRoutes {
 | 
			
		||||
      }
 | 
			
		||||
      res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get accelerations by pool');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -416,7 +416,7 @@ class MiningRoutes {
 | 
			
		||||
      const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
 | 
			
		||||
      res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get accelerations by height');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -431,7 +431,7 @@ class MiningRoutes {
 | 
			
		||||
      }
 | 
			
		||||
      res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get recent accelerations');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -446,7 +446,7 @@ class MiningRoutes {
 | 
			
		||||
      }
 | 
			
		||||
      res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get acceleration totals');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -461,7 +461,7 @@ class MiningRoutes {
 | 
			
		||||
      }
 | 
			
		||||
      res.status(200).send(Object.values(accelerationApi.getAccelerations() || {}));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get active accelerations');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -473,7 +473,7 @@ class MiningRoutes {
 | 
			
		||||
      accelerationApi.accelerationRequested(req.params.txid);
 | 
			
		||||
      res.status(200).send();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to request acceleration');
 | 
			
		||||
      handleError(req, res, 500, e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,15 @@ class PoolsParser {
 | 
			
		||||
    'addresses': '[]',
 | 
			
		||||
    'slug': 'unknown'
 | 
			
		||||
  };
 | 
			
		||||
  private uniqueLogs: string[] = [];
 | 
			
		||||
 | 
			
		||||
  private uniqueLog(loggerFunction: any, msg: string): void {
 | 
			
		||||
    if (this.uniqueLogs.includes(msg)) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.uniqueLogs.push(msg);
 | 
			
		||||
    loggerFunction(msg);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setMiningPools(pools): void {
 | 
			
		||||
    for (const pool of pools) {
 | 
			
		||||
 | 
			
		||||
@ -119,11 +119,7 @@ class RbfCache {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
 | 
			
		||||
    if ( !newTxExtended
 | 
			
		||||
      || !replaced?.length
 | 
			
		||||
      || this.txs.has(newTxExtended.txid)
 | 
			
		||||
      || !(replaced.some(tx => !this.replacedBy.has(tx.txid)))
 | 
			
		||||
    ) {
 | 
			
		||||
    if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
import { Application, Request, Response } from 'express';
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import WalletApi from './wallets';
 | 
			
		||||
import { handleError } from '../../utils/api';
 | 
			
		||||
 | 
			
		||||
class ServicesRoutes {
 | 
			
		||||
  public initRoutes(app: Application): void {
 | 
			
		||||
@ -19,7 +18,7 @@ class ServicesRoutes {
 | 
			
		||||
      const wallet = await WalletApi.getWallet(walletId);
 | 
			
		||||
      res.status(200).send(wallet);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get wallet');
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,105 +0,0 @@
 | 
			
		||||
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();
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { Application, Request, Response } from 'express';
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import statisticsApi from './statistics-api';
 | 
			
		||||
import { handleError } from '../../utils/api';
 | 
			
		||||
 | 
			
		||||
class StatisticsRoutes {
 | 
			
		||||
  public initRoutes(app: Application) {
 | 
			
		||||
    app
 | 
			
		||||
@ -65,7 +65,7 @@ class StatisticsRoutes {
 | 
			
		||||
      }
 | 
			
		||||
      res.json(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      handleError(req, res, 500, 'Failed to get statistics');
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -38,7 +38,6 @@ interface AddressTransactions {
 | 
			
		||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
 | 
			
		||||
import { calculateMempoolTxCpfp } from './cpfp';
 | 
			
		||||
import { getRecentFirstSeen } from '../utils/file-read';
 | 
			
		||||
import stratumApi, { StratumJob } from './services/stratum';
 | 
			
		||||
 | 
			
		||||
// valid 'want' subscriptions
 | 
			
		||||
const wantable = [
 | 
			
		||||
@ -404,16 +403,6 @@ class WebsocketHandler {
 | 
			
		||||
            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) {
 | 
			
		||||
            client.send(this.serializeResponse(response));
 | 
			
		||||
          }
 | 
			
		||||
@ -1395,23 +1384,6 @@ class WebsocketHandler {
 | 
			
		||||
    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
 | 
			
		||||
  // and zips it together into a valid JSON object
 | 
			
		||||
  private serializeResponse(response): string {
 | 
			
		||||
 | 
			
		||||
@ -165,10 +165,6 @@ interface IConfig {
 | 
			
		||||
  WALLETS: {
 | 
			
		||||
    ENABLED: boolean;
 | 
			
		||||
    WALLETS: string[];
 | 
			
		||||
  },
 | 
			
		||||
  STRATUM: {
 | 
			
		||||
    ENABLED: boolean;
 | 
			
		||||
    API: string;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -336,10 +332,6 @@ const defaults: IConfig = {
 | 
			
		||||
    'ENABLED': false,
 | 
			
		||||
    'WALLETS': [],
 | 
			
		||||
  },
 | 
			
		||||
  'STRATUM': {
 | 
			
		||||
    'ENABLED': false,
 | 
			
		||||
    'API': 'http://localhost:1234',
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class Config implements IConfig {
 | 
			
		||||
@ -362,7 +354,6 @@ class Config implements IConfig {
 | 
			
		||||
  REDIS: IConfig['REDIS'];
 | 
			
		||||
  FIAT_PRICE: IConfig['FIAT_PRICE'];
 | 
			
		||||
  WALLETS: IConfig['WALLETS'];
 | 
			
		||||
  STRATUM: IConfig['STRATUM'];
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    const configs = this.merge(configFromFile, defaults);
 | 
			
		||||
@ -385,7 +376,6 @@ class Config implements IConfig {
 | 
			
		||||
    this.REDIS = configs.REDIS;
 | 
			
		||||
    this.FIAT_PRICE = configs.FIAT_PRICE;
 | 
			
		||||
    this.WALLETS = configs.WALLETS;
 | 
			
		||||
    this.STRATUM = configs.STRATUM;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  merge = (...objects: object[]): IConfig => {
 | 
			
		||||
 | 
			
		||||
@ -48,7 +48,6 @@ import accelerationRoutes from './api/acceleration/acceleration.routes';
 | 
			
		||||
import aboutRoutes from './api/about.routes';
 | 
			
		||||
import mempoolBlocks from './api/mempool-blocks';
 | 
			
		||||
import walletApi from './api/services/wallets';
 | 
			
		||||
import stratumApi from './api/services/stratum';
 | 
			
		||||
 | 
			
		||||
class Server {
 | 
			
		||||
  private wss: WebSocket.Server | undefined;
 | 
			
		||||
@ -321,16 +320,11 @@ class Server {
 | 
			
		||||
    loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
 | 
			
		||||
 | 
			
		||||
    accelerationApi.connectWebsocket();
 | 
			
		||||
    if (config.STRATUM.ENABLED) {
 | 
			
		||||
      stratumApi.connectWebsocket();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setUpHttpApiRoutes(): void {
 | 
			
		||||
    bitcoinRoutes.initRoutes(this.app);
 | 
			
		||||
    if (config.MEMPOOL.OFFICIAL) {
 | 
			
		||||
      bitcoinCoreRoutes.initRoutes(this.app);
 | 
			
		||||
    }
 | 
			
		||||
    bitcoinCoreRoutes.initRoutes(this.app);
 | 
			
		||||
    pricesRoutes.initRoutes(this.app);
 | 
			
		||||
    if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) {
 | 
			
		||||
      statisticsRoutes.initRoutes(this.app);
 | 
			
		||||
 | 
			
		||||
@ -325,8 +325,6 @@ export interface BlockExtension {
 | 
			
		||||
  // Requires coinstatsindex, will be set to NULL otherwise
 | 
			
		||||
  utxoSetSize: number | null;
 | 
			
		||||
  totalInputAmt: number | null;
 | 
			
		||||
  // pools-v2.json git hash
 | 
			
		||||
  definitionHash: string | undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,6 @@ import blocks from '../api/blocks';
 | 
			
		||||
import BlocksAuditsRepository from './BlocksAuditsRepository';
 | 
			
		||||
import transactionUtils from '../api/transaction-utils';
 | 
			
		||||
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
 | 
			
		||||
import poolsUpdater from '../tasks/pools-updater';
 | 
			
		||||
 | 
			
		||||
interface DatabaseBlock {
 | 
			
		||||
  id: string;
 | 
			
		||||
@ -115,16 +114,16 @@ class BlocksRepository {
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const query = `INSERT INTO blocks(
 | 
			
		||||
        height,             hash,                     blockTimestamp,    size,
 | 
			
		||||
        weight,             tx_count,                 coinbase_raw,      difficulty,
 | 
			
		||||
        pool_id,            fees,                     fee_span,          median_fee,
 | 
			
		||||
        reward,             version,                  bits,              nonce,
 | 
			
		||||
        merkle_root,        previous_block_hash,      avg_fee,           avg_fee_rate,
 | 
			
		||||
        median_timestamp,   header,                   coinbase_address,  coinbase_addresses,
 | 
			
		||||
        coinbase_signature, utxoset_size,             utxoset_change,    avg_tx_size,
 | 
			
		||||
        total_inputs,       total_outputs,            total_input_amt,   total_output_amt,
 | 
			
		||||
        fee_percentiles,    segwit_total_txs,         segwit_total_size, segwit_total_weight,
 | 
			
		||||
        median_fee_amt,     coinbase_signature_ascii, definition_hash
 | 
			
		||||
        height,             hash,                blockTimestamp,    size,
 | 
			
		||||
        weight,             tx_count,            coinbase_raw,      difficulty,
 | 
			
		||||
        pool_id,            fees,                fee_span,          median_fee,
 | 
			
		||||
        reward,             version,             bits,              nonce,
 | 
			
		||||
        merkle_root,        previous_block_hash, avg_fee,           avg_fee_rate,
 | 
			
		||||
        median_timestamp,   header,              coinbase_address,  coinbase_addresses,
 | 
			
		||||
        coinbase_signature, utxoset_size,        utxoset_change,    avg_tx_size,
 | 
			
		||||
        total_inputs,       total_outputs,       total_input_amt,   total_output_amt,
 | 
			
		||||
        fee_percentiles,    segwit_total_txs,    segwit_total_size, segwit_total_weight,
 | 
			
		||||
        median_fee_amt,     coinbase_signature_ascii
 | 
			
		||||
      ) VALUE (
 | 
			
		||||
        ?, ?, FROM_UNIXTIME(?), ?,
 | 
			
		||||
        ?, ?, ?, ?,
 | 
			
		||||
@ -135,7 +134,7 @@ class BlocksRepository {
 | 
			
		||||
        ?, ?, ?, ?,
 | 
			
		||||
        ?, ?, ?, ?,
 | 
			
		||||
        ?, ?, ?, ?,
 | 
			
		||||
        ?, ?, ?
 | 
			
		||||
        ?, ?
 | 
			
		||||
      )`;
 | 
			
		||||
 | 
			
		||||
      const poolDbId = await PoolsRepository.$getPoolByUniqueId(block.extras.pool.id);
 | 
			
		||||
@ -182,7 +181,6 @@ class BlocksRepository {
 | 
			
		||||
        block.extras.segwitTotalWeight,
 | 
			
		||||
        block.extras.medianFeeAmt,
 | 
			
		||||
        truncatedCoinbaseSignatureAscii,
 | 
			
		||||
        poolsUpdater.currentSha
 | 
			
		||||
      ];
 | 
			
		||||
 | 
			
		||||
      await DB.query(query, params);
 | 
			
		||||
@ -1015,9 +1013,9 @@ class BlocksRepository {
 | 
			
		||||
  public async $savePool(id: string, poolId: number): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      await DB.query(`
 | 
			
		||||
        UPDATE blocks SET pool_id = ?, definition_hash = ?
 | 
			
		||||
        UPDATE blocks SET pool_id = ?
 | 
			
		||||
        WHERE hash = ?`,
 | 
			
		||||
        [poolId, poolsUpdater.currentSha, id]
 | 
			
		||||
        [poolId, id]
 | 
			
		||||
      );
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`Cannot update block pool. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
 | 
			
		||||
@ -88,8 +88,8 @@ class PoolsUpdater {
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        await DB.query('START TRANSACTION;');
 | 
			
		||||
        await this.updateDBSha(githubSha);
 | 
			
		||||
        await poolsParser.migratePoolsJson();
 | 
			
		||||
        await this.updateDBSha(githubSha);
 | 
			
		||||
        await DB.query('COMMIT;');
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, this.tag);
 | 
			
		||||
@ -121,7 +121,7 @@ class PoolsUpdater {
 | 
			
		||||
  /**
 | 
			
		||||
   * Fetch our latest pools-v2.json sha from the db
 | 
			
		||||
   */
 | 
			
		||||
  public async getShaFromDb(): Promise<string | null> {
 | 
			
		||||
  private async getShaFromDb(): Promise<string | null> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
 | 
			
		||||
      return (rows.length > 0 ? rows[0].string : null);
 | 
			
		||||
 | 
			
		||||
@ -148,10 +148,6 @@
 | 
			
		||||
    "API": "__MEMPOOL_SERVICES_API__",
 | 
			
		||||
    "ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
 | 
			
		||||
  },
 | 
			
		||||
  "STRATUM": {
 | 
			
		||||
    "ENABLED": __STRATUM_ENABLED__,
 | 
			
		||||
    "API": "__STRATUM_API__"
 | 
			
		||||
  },
 | 
			
		||||
  "REDIS": {
 | 
			
		||||
    "ENABLED": __REDIS_ENABLED__,
 | 
			
		||||
    "UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__",
 | 
			
		||||
 | 
			
		||||
@ -149,10 +149,6 @@ __REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
 | 
			
		||||
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"}
 | 
			
		||||
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
 | 
			
		||||
 | 
			
		||||
# STRATUM
 | 
			
		||||
__STRATUM_ENABLED__=${STRATUM_ENABLED:=false}
 | 
			
		||||
__STRATUM_API__=${STRATUM_API:="http://localhost:1234"}
 | 
			
		||||
 | 
			
		||||
# REDIS
 | 
			
		||||
__REDIS_ENABLED__=${REDIS_ENABLED:=false}
 | 
			
		||||
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""}
 | 
			
		||||
@ -304,10 +300,6 @@ 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_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
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
@ -11,14 +11,10 @@ services:
 | 
			
		||||
    stop_grace_period: 1m
 | 
			
		||||
    command: "./wait-for db:3306 --timeout=720 -- nginx -g 'daemon off;'"
 | 
			
		||||
    ports:
 | 
			
		||||
      - 8080:8080
 | 
			
		||||
      - 80:8080
 | 
			
		||||
  api:
 | 
			
		||||
    environment:
 | 
			
		||||
      MEMPOOL_BACKEND: "electrum"
 | 
			
		||||
      ELECTRUM_HOST: "172.27.0.1"
 | 
			
		||||
      ELECTRUM_PORT: "50001"
 | 
			
		||||
      ELECTRUM_TLS_ENABLED: "false"
 | 
			
		||||
 | 
			
		||||
      MEMPOOL_BACKEND: "none"
 | 
			
		||||
      CORE_RPC_HOST: "172.27.0.1"
 | 
			
		||||
      CORE_RPC_PORT: "8332"
 | 
			
		||||
      CORE_RPC_USERNAME: "mempool"
 | 
			
		||||
 | 
			
		||||
@ -45,7 +45,6 @@ __SERVICES_API__=${SERVICES_API:=https://mempool.space/api/v1/services}
 | 
			
		||||
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
 | 
			
		||||
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
 | 
			
		||||
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
 | 
			
		||||
__STRATUM_ENABLED__=${STRATUM_ENABLED:=false}
 | 
			
		||||
 | 
			
		||||
# Export as environment variables to be used by envsubst
 | 
			
		||||
export __MAINNET_ENABLED__
 | 
			
		||||
@ -77,7 +76,6 @@ export __SERVICES_API__
 | 
			
		||||
export __PUBLIC_ACCELERATIONS__
 | 
			
		||||
export __HISTORICAL_PRICE__
 | 
			
		||||
export __ADDITIONAL_CURRENCIES__
 | 
			
		||||
export __STRATUM_ENABLED__
 | 
			
		||||
 | 
			
		||||
folder=$(find /var/www/mempool -name "config.js" | xargs dirname)
 | 
			
		||||
echo ${folder}
 | 
			
		||||
 | 
			
		||||
@ -13,8 +13,8 @@ localhostIP="127.0.0.1"
 | 
			
		||||
cp ./docker/frontend/* ./frontend
 | 
			
		||||
cp ./nginx.conf ./frontend/
 | 
			
		||||
cp ./nginx-mempool.conf ./frontend/
 | 
			
		||||
# sed -i"" -e "s/${localhostIP}:80/0.0.0.0:__MEMPOOL_FRONTEND_HTTP_PORT__/g" ./frontend/nginx.conf
 | 
			
		||||
# sed -i"" -e "s/${localhostIP}/0.0.0.0/g" ./frontend/nginx.conf
 | 
			
		||||
sed -i"" -e "s/${localhostIP}:80/0.0.0.0:__MEMPOOL_FRONTEND_HTTP_PORT__/g" ./frontend/nginx.conf
 | 
			
		||||
sed -i"" -e "s/${localhostIP}/0.0.0.0/g" ./frontend/nginx.conf
 | 
			
		||||
sed -i"" -e "s/user nobody;//g" ./frontend/nginx.conf
 | 
			
		||||
sed -i"" -e "s!/etc/nginx/nginx-mempool.conf!/etc/nginx/conf.d/nginx-mempool.conf!g" ./frontend/nginx.conf
 | 
			
		||||
sed -i"" -e "s/${localhostIP}:8999/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/g" ./frontend/nginx-mempool.conf
 | 
			
		||||
 | 
			
		||||
@ -344,9 +344,7 @@ describe('Mainnet', () => {
 | 
			
		||||
      cy.visit('/');
 | 
			
		||||
      cy.waitForSkeletonGone();
 | 
			
		||||
 | 
			
		||||
      //TODO(knorrium): add a check for the proxied server
 | 
			
		||||
      // cy.changeNetwork('testnet4');
 | 
			
		||||
 | 
			
		||||
      cy.changeNetwork('testnet4');
 | 
			
		||||
      cy.changeNetwork('signet');
 | 
			
		||||
      cy.changeNetwork('mainnet');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,5 @@
 | 
			
		||||
  "ACCELERATOR": false,
 | 
			
		||||
  "ACCELERATOR_BUTTON": true,
 | 
			
		||||
  "PUBLIC_ACCELERATIONS": false,
 | 
			
		||||
  "STRATUM_ENABLED": false,
 | 
			
		||||
  "SERVICES_API": "https://mempool.space/api/v1/services"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										361
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										361
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -23,9 +23,9 @@
 | 
			
		||||
        "@angular/router": "^17.3.1",
 | 
			
		||||
        "@angular/ssr": "^17.3.1",
 | 
			
		||||
        "@fortawesome/angular-fontawesome": "~0.14.1",
 | 
			
		||||
        "@fortawesome/fontawesome-common-types": "~6.7.2",
 | 
			
		||||
        "@fortawesome/fontawesome-svg-core": "~6.7.2",
 | 
			
		||||
        "@fortawesome/free-solid-svg-icons": "~6.7.2",
 | 
			
		||||
        "@fortawesome/fontawesome-common-types": "~6.6.0",
 | 
			
		||||
        "@fortawesome/fontawesome-svg-core": "~6.6.0",
 | 
			
		||||
        "@fortawesome/free-solid-svg-icons": "~6.6.0",
 | 
			
		||||
        "@mempool/mempool.js": "2.3.0",
 | 
			
		||||
        "@ng-bootstrap/ng-bootstrap": "^16.0.0",
 | 
			
		||||
        "@types/qrcode": "~1.5.0",
 | 
			
		||||
@ -33,8 +33,9 @@
 | 
			
		||||
        "browserify": "^17.0.0",
 | 
			
		||||
        "clipboard": "^2.0.11",
 | 
			
		||||
        "domino": "^2.1.6",
 | 
			
		||||
        "echarts": "~5.6.0",
 | 
			
		||||
        "echarts": "~5.5.0",
 | 
			
		||||
        "esbuild": "^0.24.0",
 | 
			
		||||
        "lightweight-charts": "~3.8.0",
 | 
			
		||||
        "ngx-echarts": "~17.2.0",
 | 
			
		||||
        "ngx-infinite-scroll": "^17.0.0",
 | 
			
		||||
        "qrcode": "1.5.1",
 | 
			
		||||
@ -61,7 +62,7 @@
 | 
			
		||||
      "optionalDependencies": {
 | 
			
		||||
        "@cypress/schematic": "^2.5.0",
 | 
			
		||||
        "@types/cypress": "^1.1.3",
 | 
			
		||||
        "cypress": "^13.17.0",
 | 
			
		||||
        "cypress": "^13.15.0",
 | 
			
		||||
        "cypress-fail-on-console-error": "~5.1.0",
 | 
			
		||||
        "cypress-wait-until": "^2.0.1",
 | 
			
		||||
        "mock-socket": "~9.3.1",
 | 
			
		||||
@ -3112,10 +3113,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@cypress/request": {
 | 
			
		||||
      "version": "3.0.7",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.7.tgz",
 | 
			
		||||
      "integrity": "sha512-LzxlLEMbBOPYB85uXrDqvD4MgcenjRBLIns3zyhx7vTPj/0u2eQhzXvPiGcaJrV38Q9dbkExWp6cOHPJ+EtFYg==",
 | 
			
		||||
      "license": "Apache-2.0",
 | 
			
		||||
      "version": "3.0.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.5.tgz",
 | 
			
		||||
      "integrity": "sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "aws-sign2": "~0.7.0",
 | 
			
		||||
@ -3131,9 +3131,9 @@
 | 
			
		||||
        "json-stringify-safe": "~5.0.1",
 | 
			
		||||
        "mime-types": "~2.1.19",
 | 
			
		||||
        "performance-now": "^2.1.0",
 | 
			
		||||
        "qs": "6.13.1",
 | 
			
		||||
        "qs": "6.13.0",
 | 
			
		||||
        "safe-buffer": "^5.1.2",
 | 
			
		||||
        "tough-cookie": "^5.0.0",
 | 
			
		||||
        "tough-cookie": "^4.1.3",
 | 
			
		||||
        "tunnel-agent": "^0.6.0",
 | 
			
		||||
        "uuid": "^8.3.2"
 | 
			
		||||
      },
 | 
			
		||||
@ -3141,22 +3141,6 @@
 | 
			
		||||
        "node": ">= 6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@cypress/request/node_modules/qs": {
 | 
			
		||||
      "version": "6.13.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz",
 | 
			
		||||
      "integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==",
 | 
			
		||||
      "license": "BSD-3-Clause",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "side-channel": "^1.0.6"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=0.6"
 | 
			
		||||
      },
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "url": "https://github.com/sponsors/ljharb"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@cypress/schematic": {
 | 
			
		||||
      "version": "2.5.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@cypress/schematic/-/schematic-2.5.0.tgz",
 | 
			
		||||
@ -3690,33 +3674,30 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@fortawesome/fontawesome-common-types": {
 | 
			
		||||
      "version": "6.7.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
 | 
			
		||||
      "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "version": "6.6.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
 | 
			
		||||
      "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@fortawesome/fontawesome-svg-core": {
 | 
			
		||||
      "version": "6.7.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
 | 
			
		||||
      "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "version": "6.6.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
 | 
			
		||||
      "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@fortawesome/fontawesome-common-types": "6.7.2"
 | 
			
		||||
        "@fortawesome/fontawesome-common-types": "6.6.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@fortawesome/free-solid-svg-icons": {
 | 
			
		||||
      "version": "6.7.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
 | 
			
		||||
      "integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==",
 | 
			
		||||
      "license": "(CC-BY-4.0 AND MIT)",
 | 
			
		||||
      "version": "6.6.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
 | 
			
		||||
      "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@fortawesome/fontawesome-common-types": "6.7.2"
 | 
			
		||||
        "@fortawesome/fontawesome-common-types": "6.6.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6"
 | 
			
		||||
@ -5692,7 +5673,6 @@
 | 
			
		||||
      "version": "0.2.6",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
 | 
			
		||||
      "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "safer-buffer": "~2.1.0"
 | 
			
		||||
@ -5727,7 +5707,6 @@
 | 
			
		||||
      "version": "1.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=0.8"
 | 
			
		||||
@ -5848,7 +5827,6 @@
 | 
			
		||||
      "version": "0.7.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
 | 
			
		||||
      "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
 | 
			
		||||
      "license": "Apache-2.0",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": "*"
 | 
			
		||||
@ -5858,7 +5836,6 @@
 | 
			
		||||
      "version": "1.13.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz",
 | 
			
		||||
      "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/axios": {
 | 
			
		||||
@ -6016,7 +5993,6 @@
 | 
			
		||||
      "version": "1.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
 | 
			
		||||
      "license": "BSD-3-Clause",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "tweetnacl": "^0.14.3"
 | 
			
		||||
@ -7092,7 +7068,6 @@
 | 
			
		||||
      "version": "0.12.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
 | 
			
		||||
      "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
 | 
			
		||||
      "license": "Apache-2.0",
 | 
			
		||||
      "optional": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/chai": {
 | 
			
		||||
@ -7195,16 +7170,15 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/ci-info": {
 | 
			
		||||
      "version": "4.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==",
 | 
			
		||||
      "version": "3.8.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
 | 
			
		||||
      "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
 | 
			
		||||
      "funding": [
 | 
			
		||||
        {
 | 
			
		||||
          "type": "github",
 | 
			
		||||
          "url": "https://github.com/sponsors/sibiraj-s"
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=8"
 | 
			
		||||
@ -7979,14 +7953,13 @@
 | 
			
		||||
      "peer": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/cypress": {
 | 
			
		||||
      "version": "13.17.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.17.0.tgz",
 | 
			
		||||
      "integrity": "sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==",
 | 
			
		||||
      "version": "13.15.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.0.tgz",
 | 
			
		||||
      "integrity": "sha512-53aO7PwOfi604qzOkCSzNlWquCynLlKE/rmmpSPcziRH6LNfaDUAklQT6WJIsD8ywxlIy+uVZsnTMCCQVd2kTw==",
 | 
			
		||||
      "hasInstallScript": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@cypress/request": "^3.0.6",
 | 
			
		||||
        "@cypress/request": "^3.0.4",
 | 
			
		||||
        "@cypress/xvfb": "^1.2.4",
 | 
			
		||||
        "@types/sinonjs__fake-timers": "8.1.1",
 | 
			
		||||
        "@types/sizzle": "^2.3.2",
 | 
			
		||||
@ -7997,7 +7970,6 @@
 | 
			
		||||
        "cachedir": "^2.3.0",
 | 
			
		||||
        "chalk": "^4.1.0",
 | 
			
		||||
        "check-more-types": "^2.24.0",
 | 
			
		||||
        "ci-info": "^4.0.0",
 | 
			
		||||
        "cli-cursor": "^3.1.0",
 | 
			
		||||
        "cli-table3": "~0.6.1",
 | 
			
		||||
        "commander": "^6.2.1",
 | 
			
		||||
@ -8012,6 +7984,7 @@
 | 
			
		||||
        "figures": "^3.2.0",
 | 
			
		||||
        "fs-extra": "^9.1.0",
 | 
			
		||||
        "getos": "^3.2.1",
 | 
			
		||||
        "is-ci": "^3.0.1",
 | 
			
		||||
        "is-installed-globally": "~0.4.0",
 | 
			
		||||
        "lazy-ass": "^1.6.0",
 | 
			
		||||
        "listr2": "^3.8.3",
 | 
			
		||||
@ -8026,7 +7999,6 @@
 | 
			
		||||
        "semver": "^7.5.3",
 | 
			
		||||
        "supports-color": "^8.1.1",
 | 
			
		||||
        "tmp": "~0.2.3",
 | 
			
		||||
        "tree-kill": "1.2.2",
 | 
			
		||||
        "untildify": "^4.0.0",
 | 
			
		||||
        "yauzl": "^2.10.0"
 | 
			
		||||
      },
 | 
			
		||||
@ -8229,7 +8201,6 @@
 | 
			
		||||
      "version": "1.14.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
 | 
			
		||||
      "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "assert-plus": "^1.0.0"
 | 
			
		||||
@ -8716,7 +8687,6 @@
 | 
			
		||||
      "version": "0.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "jsbn": "~0.1.0",
 | 
			
		||||
@ -8724,12 +8694,12 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/echarts": {
 | 
			
		||||
      "version": "5.6.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
 | 
			
		||||
      "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
 | 
			
		||||
      "version": "5.5.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz",
 | 
			
		||||
      "integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "tslib": "2.3.0",
 | 
			
		||||
        "zrender": "5.6.1"
 | 
			
		||||
        "zrender": "5.5.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/echarts/node_modules/tslib": {
 | 
			
		||||
@ -9935,7 +9905,6 @@
 | 
			
		||||
      "engines": [
 | 
			
		||||
        "node >=0.6.0"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/falafel": {
 | 
			
		||||
@ -9952,6 +9921,11 @@
 | 
			
		||||
        "node": ">=0.4.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/fancy-canvas": {
 | 
			
		||||
      "version": "0.2.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-0.2.2.tgz",
 | 
			
		||||
      "integrity": "sha512-50qi8xA0QkHbjmb8h7XQ6k2fvD7y/yMfiUw9YTarJ7rWrq6o5/3CCXPouYk+XSLASvvxtjyiQLRBFt3qkE3oyA=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/fast-deep-equal": {
 | 
			
		||||
      "version": "3.1.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
 | 
			
		||||
@ -10219,7 +10193,6 @@
 | 
			
		||||
      "version": "0.6.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
 | 
			
		||||
      "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
 | 
			
		||||
      "license": "Apache-2.0",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": "*"
 | 
			
		||||
@ -10427,7 +10400,6 @@
 | 
			
		||||
      "version": "0.1.7",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
 | 
			
		||||
      "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "assert-plus": "^1.0.0"
 | 
			
		||||
@ -10882,7 +10854,6 @@
 | 
			
		||||
      "version": "1.4.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz",
 | 
			
		||||
      "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "assert-plus": "^1.0.0",
 | 
			
		||||
@ -11249,6 +11220,18 @@
 | 
			
		||||
        "url": "https://github.com/sponsors/ljharb"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/is-ci": {
 | 
			
		||||
      "version": "3.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "ci-info": "^3.2.0"
 | 
			
		||||
      },
 | 
			
		||||
      "bin": {
 | 
			
		||||
        "is-ci": "bin.js"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/is-core-module": {
 | 
			
		||||
      "version": "2.13.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
 | 
			
		||||
@ -11498,7 +11481,6 @@
 | 
			
		||||
      "version": "1.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/is-unicode-supported": {
 | 
			
		||||
@ -11563,7 +11545,6 @@
 | 
			
		||||
      "version": "0.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/istanbul-lib-coverage": {
 | 
			
		||||
@ -11697,7 +11678,6 @@
 | 
			
		||||
      "version": "0.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
 | 
			
		||||
      "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/jsesc": {
 | 
			
		||||
@ -11726,7 +11706,6 @@
 | 
			
		||||
      "version": "0.4.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
 | 
			
		||||
      "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
 | 
			
		||||
      "license": "(AFL-2.1 OR BSD-3-Clause)",
 | 
			
		||||
      "optional": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/json-schema-traverse": {
 | 
			
		||||
@ -11744,7 +11723,6 @@
 | 
			
		||||
      "version": "5.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
 | 
			
		||||
      "license": "ISC",
 | 
			
		||||
      "optional": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/json5": {
 | 
			
		||||
@ -11805,7 +11783,6 @@
 | 
			
		||||
      "engines": [
 | 
			
		||||
        "node >=0.6.0"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "assert-plus": "1.0.0",
 | 
			
		||||
@ -12129,6 +12106,14 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/lightweight-charts": {
 | 
			
		||||
      "version": "3.8.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-3.8.0.tgz",
 | 
			
		||||
      "integrity": "sha512-7yFGnYuE1RjRJG9RwUTBz5wvF1QtjBOSW4FFlikr8Dh+/TDNt4ci+HsWSYmStgQUpawpvkCJ3j5/W25GppGj9Q==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "fancy-canvas": "0.2.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/limiter": {
 | 
			
		||||
      "version": "1.1.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
 | 
			
		||||
@ -14125,7 +14110,6 @@
 | 
			
		||||
      "version": "2.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/picocolors": {
 | 
			
		||||
@ -14556,6 +14540,12 @@
 | 
			
		||||
        "node": ">= 0.10"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/psl": {
 | 
			
		||||
      "version": "1.9.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
 | 
			
		||||
      "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
 | 
			
		||||
      "optional": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/public-encrypt": {
 | 
			
		||||
      "version": "4.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
 | 
			
		||||
@ -14671,6 +14661,12 @@
 | 
			
		||||
        "node": ">=0.4.x"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/querystringify": {
 | 
			
		||||
      "version": "2.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
 | 
			
		||||
      "optional": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/queue-microtask": {
 | 
			
		||||
      "version": "1.2.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
 | 
			
		||||
@ -16032,7 +16028,6 @@
 | 
			
		||||
      "version": "1.18.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
 | 
			
		||||
      "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "asn1": "~0.2.3",
 | 
			
		||||
@ -16582,26 +16577,6 @@
 | 
			
		||||
        "readable-stream": "3"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/tldts": {
 | 
			
		||||
      "version": "6.1.70",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.70.tgz",
 | 
			
		||||
      "integrity": "sha512-/W1YVgYVJd9ZDjey5NXadNh0mJXkiUMUue9Zebd0vpdo1sU+H4zFFTaJ1RKD4N6KFoHfcXy6l+Vu7bh+bdWCzA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "tldts-core": "^6.1.70"
 | 
			
		||||
      },
 | 
			
		||||
      "bin": {
 | 
			
		||||
        "tldts": "bin/cli.js"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/tldts-core": {
 | 
			
		||||
      "version": "6.1.70",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.70.tgz",
 | 
			
		||||
      "integrity": "sha512-RNnIXDB1FD4T9cpQRErEqw6ZpjLlGdMOitdV+0xtbsnwr4YFka1zpc7D4KD+aAn8oSG5JyFrdasZTE04qDE9Yg==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/tlite": {
 | 
			
		||||
      "version": "0.1.9",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tlite/-/tlite-0.1.9.tgz",
 | 
			
		||||
@ -16646,16 +16621,27 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/tough-cookie": {
 | 
			
		||||
      "version": "5.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==",
 | 
			
		||||
      "license": "BSD-3-Clause",
 | 
			
		||||
      "version": "4.1.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
 | 
			
		||||
      "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "tldts": "^6.1.32"
 | 
			
		||||
        "psl": "^1.1.33",
 | 
			
		||||
        "punycode": "^2.1.1",
 | 
			
		||||
        "universalify": "^0.2.0",
 | 
			
		||||
        "url-parse": "^1.5.3"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=16"
 | 
			
		||||
        "node": ">=6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/tough-cookie/node_modules/universalify": {
 | 
			
		||||
      "version": "0.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">= 4.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/transform-ast": {
 | 
			
		||||
@ -16824,7 +16810,6 @@
 | 
			
		||||
      "version": "0.6.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
 | 
			
		||||
      "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
 | 
			
		||||
      "license": "Apache-2.0",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "safe-buffer": "^5.0.1"
 | 
			
		||||
@ -16837,7 +16822,6 @@
 | 
			
		||||
      "version": "0.14.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
 | 
			
		||||
      "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
 | 
			
		||||
      "license": "Unlicense",
 | 
			
		||||
      "optional": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/type": {
 | 
			
		||||
@ -17146,6 +17130,16 @@
 | 
			
		||||
        "querystring": "0.2.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/url-parse": {
 | 
			
		||||
      "version": "1.5.10",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
 | 
			
		||||
      "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "querystringify": "^2.1.1",
 | 
			
		||||
        "requires-port": "^1.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/url/node_modules/punycode": {
 | 
			
		||||
      "version": "1.3.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
 | 
			
		||||
@ -17213,7 +17207,6 @@
 | 
			
		||||
      "engines": [
 | 
			
		||||
        "node >=0.6.0"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "assert-plus": "^1.0.0",
 | 
			
		||||
@ -18366,9 +18359,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/zrender": {
 | 
			
		||||
      "version": "5.6.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
 | 
			
		||||
      "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
 | 
			
		||||
      "version": "5.5.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz",
 | 
			
		||||
      "integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "tslib": "2.3.0"
 | 
			
		||||
      }
 | 
			
		||||
@ -20355,9 +20348,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "@cypress/request": {
 | 
			
		||||
      "version": "3.0.7",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.7.tgz",
 | 
			
		||||
      "integrity": "sha512-LzxlLEMbBOPYB85uXrDqvD4MgcenjRBLIns3zyhx7vTPj/0u2eQhzXvPiGcaJrV38Q9dbkExWp6cOHPJ+EtFYg==",
 | 
			
		||||
      "version": "3.0.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.5.tgz",
 | 
			
		||||
      "integrity": "sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "aws-sign2": "~0.7.0",
 | 
			
		||||
@ -20373,22 +20366,11 @@
 | 
			
		||||
        "json-stringify-safe": "~5.0.1",
 | 
			
		||||
        "mime-types": "~2.1.19",
 | 
			
		||||
        "performance-now": "^2.1.0",
 | 
			
		||||
        "qs": "6.13.1",
 | 
			
		||||
        "qs": "6.13.0",
 | 
			
		||||
        "safe-buffer": "^5.1.2",
 | 
			
		||||
        "tough-cookie": "^5.0.0",
 | 
			
		||||
        "tough-cookie": "^4.1.3",
 | 
			
		||||
        "tunnel-agent": "^0.6.0",
 | 
			
		||||
        "uuid": "^8.3.2"
 | 
			
		||||
      },
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "qs": {
 | 
			
		||||
          "version": "6.13.1",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz",
 | 
			
		||||
          "integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==",
 | 
			
		||||
          "optional": true,
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "side-channel": "^1.0.6"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "@cypress/schematic": {
 | 
			
		||||
@ -20667,24 +20649,24 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "@fortawesome/fontawesome-common-types": {
 | 
			
		||||
      "version": "6.7.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
 | 
			
		||||
      "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg=="
 | 
			
		||||
      "version": "6.6.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
 | 
			
		||||
      "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw=="
 | 
			
		||||
    },
 | 
			
		||||
    "@fortawesome/fontawesome-svg-core": {
 | 
			
		||||
      "version": "6.7.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
 | 
			
		||||
      "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
 | 
			
		||||
      "version": "6.6.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
 | 
			
		||||
      "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "@fortawesome/fontawesome-common-types": "6.7.2"
 | 
			
		||||
        "@fortawesome/fontawesome-common-types": "6.6.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "@fortawesome/free-solid-svg-icons": {
 | 
			
		||||
      "version": "6.7.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
 | 
			
		||||
      "integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==",
 | 
			
		||||
      "version": "6.6.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
 | 
			
		||||
      "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "@fortawesome/fontawesome-common-types": "6.7.2"
 | 
			
		||||
        "@fortawesome/fontawesome-common-types": "6.6.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "@goto-bus-stop/common-shake": {
 | 
			
		||||
@ -23316,9 +23298,9 @@
 | 
			
		||||
      "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg=="
 | 
			
		||||
    },
 | 
			
		||||
    "ci-info": {
 | 
			
		||||
      "version": "4.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==",
 | 
			
		||||
      "version": "3.8.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
 | 
			
		||||
      "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
 | 
			
		||||
      "optional": true
 | 
			
		||||
    },
 | 
			
		||||
    "cipher-base": {
 | 
			
		||||
@ -23914,12 +23896,12 @@
 | 
			
		||||
      "peer": true
 | 
			
		||||
    },
 | 
			
		||||
    "cypress": {
 | 
			
		||||
      "version": "13.17.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.17.0.tgz",
 | 
			
		||||
      "integrity": "sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==",
 | 
			
		||||
      "version": "13.15.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.0.tgz",
 | 
			
		||||
      "integrity": "sha512-53aO7PwOfi604qzOkCSzNlWquCynLlKE/rmmpSPcziRH6LNfaDUAklQT6WJIsD8ywxlIy+uVZsnTMCCQVd2kTw==",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "@cypress/request": "^3.0.6",
 | 
			
		||||
        "@cypress/request": "^3.0.4",
 | 
			
		||||
        "@cypress/xvfb": "^1.2.4",
 | 
			
		||||
        "@types/sinonjs__fake-timers": "8.1.1",
 | 
			
		||||
        "@types/sizzle": "^2.3.2",
 | 
			
		||||
@ -23930,7 +23912,6 @@
 | 
			
		||||
        "cachedir": "^2.3.0",
 | 
			
		||||
        "chalk": "^4.1.0",
 | 
			
		||||
        "check-more-types": "^2.24.0",
 | 
			
		||||
        "ci-info": "^4.0.0",
 | 
			
		||||
        "cli-cursor": "^3.1.0",
 | 
			
		||||
        "cli-table3": "~0.6.1",
 | 
			
		||||
        "commander": "^6.2.1",
 | 
			
		||||
@ -23945,6 +23926,7 @@
 | 
			
		||||
        "figures": "^3.2.0",
 | 
			
		||||
        "fs-extra": "^9.1.0",
 | 
			
		||||
        "getos": "^3.2.1",
 | 
			
		||||
        "is-ci": "^3.0.1",
 | 
			
		||||
        "is-installed-globally": "~0.4.0",
 | 
			
		||||
        "lazy-ass": "^1.6.0",
 | 
			
		||||
        "listr2": "^3.8.3",
 | 
			
		||||
@ -23959,7 +23941,6 @@
 | 
			
		||||
        "semver": "^7.5.3",
 | 
			
		||||
        "supports-color": "^8.1.1",
 | 
			
		||||
        "tmp": "~0.2.3",
 | 
			
		||||
        "tree-kill": "1.2.2",
 | 
			
		||||
        "untildify": "^4.0.0",
 | 
			
		||||
        "yauzl": "^2.10.0"
 | 
			
		||||
      },
 | 
			
		||||
@ -24485,12 +24466,12 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "echarts": {
 | 
			
		||||
      "version": "5.6.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
 | 
			
		||||
      "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
 | 
			
		||||
      "version": "5.5.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz",
 | 
			
		||||
      "integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "tslib": "2.3.0",
 | 
			
		||||
        "zrender": "5.6.1"
 | 
			
		||||
        "zrender": "5.5.0"
 | 
			
		||||
      },
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "tslib": {
 | 
			
		||||
@ -25452,6 +25433,11 @@
 | 
			
		||||
        "object-keys": "^1.0.6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "fancy-canvas": {
 | 
			
		||||
      "version": "0.2.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-0.2.2.tgz",
 | 
			
		||||
      "integrity": "sha512-50qi8xA0QkHbjmb8h7XQ6k2fvD7y/yMfiUw9YTarJ7rWrq6o5/3CCXPouYk+XSLASvvxtjyiQLRBFt3qkE3oyA=="
 | 
			
		||||
    },
 | 
			
		||||
    "fast-deep-equal": {
 | 
			
		||||
      "version": "3.1.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
 | 
			
		||||
@ -26387,6 +26373,15 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz",
 | 
			
		||||
      "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "is-ci": {
 | 
			
		||||
      "version": "3.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "ci-info": "^3.2.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "is-core-module": {
 | 
			
		||||
      "version": "2.13.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
 | 
			
		||||
@ -27020,6 +27015,14 @@
 | 
			
		||||
        "webpack-sources": "^3.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "lightweight-charts": {
 | 
			
		||||
      "version": "3.8.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-3.8.0.tgz",
 | 
			
		||||
      "integrity": "sha512-7yFGnYuE1RjRJG9RwUTBz5wvF1QtjBOSW4FFlikr8Dh+/TDNt4ci+HsWSYmStgQUpawpvkCJ3j5/W25GppGj9Q==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "fancy-canvas": "0.2.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "limiter": {
 | 
			
		||||
      "version": "1.1.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
 | 
			
		||||
@ -28803,6 +28806,12 @@
 | 
			
		||||
        "event-stream": "=3.3.4"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "psl": {
 | 
			
		||||
      "version": "1.9.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
 | 
			
		||||
      "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
 | 
			
		||||
      "optional": true
 | 
			
		||||
    },
 | 
			
		||||
    "public-encrypt": {
 | 
			
		||||
      "version": "4.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
 | 
			
		||||
@ -28894,6 +28903,12 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
 | 
			
		||||
      "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM="
 | 
			
		||||
    },
 | 
			
		||||
    "querystringify": {
 | 
			
		||||
      "version": "2.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
 | 
			
		||||
      "optional": true
 | 
			
		||||
    },
 | 
			
		||||
    "queue-microtask": {
 | 
			
		||||
      "version": "1.2.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
 | 
			
		||||
@ -30358,21 +30373,6 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "tldts": {
 | 
			
		||||
      "version": "6.1.70",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.70.tgz",
 | 
			
		||||
      "integrity": "sha512-/W1YVgYVJd9ZDjey5NXadNh0mJXkiUMUue9Zebd0vpdo1sU+H4zFFTaJ1RKD4N6KFoHfcXy6l+Vu7bh+bdWCzA==",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "tldts-core": "^6.1.70"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "tldts-core": {
 | 
			
		||||
      "version": "6.1.70",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.70.tgz",
 | 
			
		||||
      "integrity": "sha512-RNnIXDB1FD4T9cpQRErEqw6ZpjLlGdMOitdV+0xtbsnwr4YFka1zpc7D4KD+aAn8oSG5JyFrdasZTE04qDE9Yg==",
 | 
			
		||||
      "optional": true
 | 
			
		||||
    },
 | 
			
		||||
    "tlite": {
 | 
			
		||||
      "version": "0.1.9",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tlite/-/tlite-0.1.9.tgz",
 | 
			
		||||
@ -30405,12 +30405,23 @@
 | 
			
		||||
      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
 | 
			
		||||
    },
 | 
			
		||||
    "tough-cookie": {
 | 
			
		||||
      "version": "5.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==",
 | 
			
		||||
      "version": "4.1.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
 | 
			
		||||
      "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "tldts": "^6.1.32"
 | 
			
		||||
        "psl": "^1.1.33",
 | 
			
		||||
        "punycode": "^2.1.1",
 | 
			
		||||
        "universalify": "^0.2.0",
 | 
			
		||||
        "url-parse": "^1.5.3"
 | 
			
		||||
      },
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "universalify": {
 | 
			
		||||
          "version": "0.2.0",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
 | 
			
		||||
          "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
 | 
			
		||||
          "optional": true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "transform-ast": {
 | 
			
		||||
@ -30746,6 +30757,16 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "url-parse": {
 | 
			
		||||
      "version": "1.5.10",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
 | 
			
		||||
      "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "querystringify": "^2.1.1",
 | 
			
		||||
        "requires-port": "^1.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "util-deprecate": {
 | 
			
		||||
      "version": "1.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
 | 
			
		||||
@ -31485,9 +31506,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "zrender": {
 | 
			
		||||
      "version": "5.6.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
 | 
			
		||||
      "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
 | 
			
		||||
      "version": "5.5.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz",
 | 
			
		||||
      "integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "tslib": "2.3.0"
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
@ -76,9 +76,9 @@
 | 
			
		||||
    "@angular/router": "^17.3.1",
 | 
			
		||||
    "@angular/ssr": "^17.3.1",
 | 
			
		||||
    "@fortawesome/angular-fontawesome": "~0.14.1",
 | 
			
		||||
    "@fortawesome/fontawesome-common-types": "~6.7.2",
 | 
			
		||||
    "@fortawesome/fontawesome-svg-core": "~6.7.2",
 | 
			
		||||
    "@fortawesome/free-solid-svg-icons": "~6.7.2",
 | 
			
		||||
    "@fortawesome/fontawesome-common-types": "~6.6.0",
 | 
			
		||||
    "@fortawesome/fontawesome-svg-core": "~6.6.0",
 | 
			
		||||
    "@fortawesome/free-solid-svg-icons": "~6.6.0",
 | 
			
		||||
    "@mempool/mempool.js": "2.3.0",
 | 
			
		||||
    "@ng-bootstrap/ng-bootstrap": "^16.0.0",
 | 
			
		||||
    "@types/qrcode": "~1.5.0",
 | 
			
		||||
@ -86,7 +86,8 @@
 | 
			
		||||
    "browserify": "^17.0.0",
 | 
			
		||||
    "clipboard": "^2.0.11",
 | 
			
		||||
    "domino": "^2.1.6",
 | 
			
		||||
    "echarts": "~5.6.0",
 | 
			
		||||
    "echarts": "~5.5.0",
 | 
			
		||||
    "lightweight-charts": "~3.8.0",
 | 
			
		||||
    "ngx-echarts": "~17.2.0",
 | 
			
		||||
    "ngx-infinite-scroll": "^17.0.0",
 | 
			
		||||
    "qrcode": "1.5.1",
 | 
			
		||||
@ -114,7 +115,7 @@
 | 
			
		||||
  "optionalDependencies": {
 | 
			
		||||
    "@cypress/schematic": "^2.5.0",
 | 
			
		||||
    "@types/cypress": "^1.1.3",
 | 
			
		||||
    "cypress": "^13.17.0",
 | 
			
		||||
    "cypress": "^13.15.0",
 | 
			
		||||
    "cypress-fail-on-console-error": "~5.1.0",
 | 
			
		||||
    "cypress-wait-until": "^2.0.1",
 | 
			
		||||
    "mock-socket": "~9.3.1",
 | 
			
		||||
 | 
			
		||||
@ -3,10 +3,8 @@ const fs = require('fs');
 | 
			
		||||
let PROXY_CONFIG = require('./proxy.conf');
 | 
			
		||||
 | 
			
		||||
PROXY_CONFIG.forEach(entry => {
 | 
			
		||||
  const hostname = process.env.CYPRESS_REROUTE_TESTNET === 'true' ? 'mempool-staging.fra.mempool.space' : 'node201.fmt.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");
 | 
			
		||||
  entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space");
 | 
			
		||||
  entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
module.exports = PROXY_CONFIG;
 | 
			
		||||
 | 
			
		||||
@ -439,39 +439,4 @@ export const fiatCurrencies = {
 | 
			
		||||
    code: 'ZAR',
 | 
			
		||||
    indexed: true,
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface Timezone {
 | 
			
		||||
  offset: string;
 | 
			
		||||
  name: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const timezones: Timezone[] = [
 | 
			
		||||
  { offset: '-12', name: 'Anywhere on Earth (AoE)' },
 | 
			
		||||
  { offset: '-11', name: 'Samoa Standard Time (SST)' },
 | 
			
		||||
  { offset: '-10', name: 'Hawaii Standard Time (HST)' },
 | 
			
		||||
  { offset: '-9', name: 'Alaska Standard Time (AKST)' },
 | 
			
		||||
  { offset: '-8', name: 'Pacific Standard Time (PST)' },
 | 
			
		||||
  { offset: '-7', name: 'Mountain Standard Time (MST)' },
 | 
			
		||||
  { offset: '-6', name: 'Central Standard Time (CST)' },
 | 
			
		||||
  { offset: '-5', name: 'Eastern Standard Time (EST)' },
 | 
			
		||||
  { offset: '-4', name: 'Atlantic Standard Time (AST)' },
 | 
			
		||||
  { offset: '-3', name: 'Argentina Time (ART)' },
 | 
			
		||||
  { offset: '-2', name: 'Fernando de Noronha Time (FNT)' },
 | 
			
		||||
  { offset: '-1', name: 'Azores Time (AZOT)' },
 | 
			
		||||
  { offset: '+0', name: 'Greenwich Mean Time (GMT)' },
 | 
			
		||||
  { offset: '+1', name: 'Central European Time (CET)' },
 | 
			
		||||
  { offset: '+2', name: 'Eastern European Time (EET)' },
 | 
			
		||||
  { offset: '+3', name: 'Moscow Standard Time (MSK)' },
 | 
			
		||||
  { offset: '+4', name: 'Armenia Time (AMT)' },
 | 
			
		||||
  { offset: '+5', name: 'Pakistan Standard Time (PKT)' },
 | 
			
		||||
  { offset: '+6', name: 'Xinjiang Time (XJT)' },
 | 
			
		||||
  { offset: '+7', name: 'Indochina Time (ICT)' },
 | 
			
		||||
  { offset: '+8', name: 'Hong Kong Time (HKT)' },
 | 
			
		||||
  { offset: '+9', name: 'Japan Standard Time (JST)' },
 | 
			
		||||
  { offset: '+10', name: 'Australian Eastern Standard Time (AEST)' },
 | 
			
		||||
  { offset: '+11', name: 'Norfolk Time (NFT)' },
 | 
			
		||||
  { offset: '+12', name: 'New Zealand Standard Time (NZST)' },
 | 
			
		||||
  { offset: '+13', name: 'Tonga Time (TOT)' },
 | 
			
		||||
  { offset: '+14', name: 'Line Islands Time (LINT)' }
 | 
			
		||||
];
 | 
			
		||||
};
 | 
			
		||||
@ -217,7 +217,7 @@
 | 
			
		||||
          <ng-container>
 | 
			
		||||
            <ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
 | 
			
		||||
              <a [href]="'https://x.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
 | 
			
		||||
                <img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
 | 
			
		||||
                <img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
 | 
			
		||||
              </a>
 | 
			
		||||
            </ng-template>
 | 
			
		||||
          </ng-container>
 | 
			
		||||
@ -229,7 +229,7 @@
 | 
			
		||||
        <div class="wrapper">
 | 
			
		||||
          <ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
 | 
			
		||||
            <a [href]="'https://x.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
 | 
			
		||||
              <img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
 | 
			
		||||
              <img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
 | 
			
		||||
            </a>
 | 
			
		||||
          </ng-template>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
@ -1,18 +1,10 @@
 | 
			
		||||
<div class="box card w-100 accelerate-checkout-inner" [class.input-disabled]="isCheckoutLocked > 0" style="background: var(--box-bg)" id=acceleratePreviewAnchor>
 | 
			
		||||
<div class="box card w-100" style="background: var(--box-bg)" id=acceleratePreviewAnchor>
 | 
			
		||||
  @if (accelerateError) {
 | 
			
		||||
    @if (accelerateError.includes('Payment declined')) {
 | 
			
		||||
      <div class="row mb-1 text-center">
 | 
			
		||||
        <div class="col-sm">
 | 
			
		||||
          <h1 style="font-size: larger;">{{ accelerateError }}</h1>
 | 
			
		||||
        </div>
 | 
			
		||||
    <div class="row mb-1 text-center">
 | 
			
		||||
      <div class="col-sm">
 | 
			
		||||
        <h1 style="font-size: larger;" i18n="accelerator.sorry-error-title">Sorry, something went wrong!</h1>
 | 
			
		||||
      </div>
 | 
			
		||||
    } @else {
 | 
			
		||||
      <div class="row mb-1 text-center">
 | 
			
		||||
        <div class="col-sm">
 | 
			
		||||
          <h1 style="font-size: larger;" i18n="accelerator.sorry-error-title">Sorry, something went wrong!</h1>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    }
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="row text-center mt-1">
 | 
			
		||||
      <div class="col-sm">
 | 
			
		||||
        <div class="d-flex flex-row justify-content-center align-items-center">
 | 
			
		||||
@ -365,11 +357,11 @@
 | 
			
		||||
          <app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true" class="ml-2"></app-active-acceleration-box>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="payment-area" style="font-size: 14px;">
 | 
			
		||||
      <div class="payment-area mt-2 p-2" style="font-size: 14px;">
 | 
			
		||||
        <div class="row text-center justify-content-center mx-2">
 | 
			
		||||
          <span 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></span>
 | 
			
		||||
          <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>
 | 
			
		||||
        @if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay)) {
 | 
			
		||||
        @if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) {
 | 
			
		||||
          <div class="row">
 | 
			
		||||
            <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>
 | 
			
		||||
@ -386,12 +378,9 @@
 | 
			
		||||
                  <p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container> <span><small style="font-family: monospace;">{{ ((invoice.btcDue * 100_000_000) || cost) | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></span></p>
 | 
			
		||||
                  <app-bitcoin-invoice style="width: 100%;" [invoice]="invoice" [minimal]="true" (completed)="bitcoinPaymentCompleted()"></app-bitcoin-invoice>
 | 
			
		||||
                } @else if (btcpayInvoiceFailed) {
 | 
			
		||||
                  <div class="btcpay-invoice">
 | 
			
		||||
                    <fa-icon style="font-size: 20px; color: var(--red)" [icon]="['fas', 'circle-xmark']"></fa-icon>
 | 
			
		||||
                    <span i18n="accelerator.failed-to-load-invoice">Failed to load invoice</span>
 | 
			
		||||
                    @if (!loadingBtcpayInvoice) {
 | 
			
		||||
                      <button class="btn btn-sm btn-secondary mt-0 mt-md-1" (click)="requestBTCPayInvoice()">Retry ↻</button>
 | 
			
		||||
                    }
 | 
			
		||||
                  <p i18n="accelerator.failed-to-load-invoice">Failed to load invoice</p>
 | 
			
		||||
                  <div class="d-flex flex-column align-items-center justify-content-center" style="width: 100%; height: 292px;">
 | 
			
		||||
                    <fa-icon style="font-size: 24px; color: var(--red)" [icon]="['fas', 'circle-xmark']"></fa-icon>
 | 
			
		||||
                  </div>
 | 
			
		||||
                } @else {
 | 
			
		||||
                  <p i18n="accelerator.loading-invoice">Loading invoice...</p>
 | 
			
		||||
@ -400,13 +389,13 @@
 | 
			
		||||
                  </div>
 | 
			
		||||
                }
 | 
			
		||||
              </div>
 | 
			
		||||
              @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {
 | 
			
		||||
              @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
 | 
			
		||||
                <div class="col-sm text-center flex-grow-0  d-flex flex-column justify-content-center align-items-center">
 | 
			
		||||
                  <p class="text-nowrap">——<span i18n="or"> OR </span>——</p>
 | 
			
		||||
                  <p class="text-nowrap">—<span i18n="or">OR</span>—</p>
 | 
			
		||||
                </div>
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {
 | 
			
		||||
            @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
 | 
			
		||||
              <div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
 | 
			
		||||
                <p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container> <app-fiat [value]="cost"></app-fiat> with</p>
 | 
			
		||||
                @if (canPayWithCashapp) {
 | 
			
		||||
@ -424,17 +413,6 @@
 | 
			
		||||
                    <img src="/resources/google-pay.png" height=37>
 | 
			
		||||
                  </div>
 | 
			
		||||
                }
 | 
			
		||||
                @if (canPayWithCardOnFile) {
 | 
			
		||||
                  @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) { <span class="mt-1 mb-1"></span> }
 | 
			
		||||
                  <div class="paymentMethod mx-2 d-flex justify-content-center align-items-center" style="width: 200px; height: 55px" (click)="moveToStep('cardonfile')">
 | 
			
		||||
                    @if (['VISA', 'MASTERCARD', 'JCB', 'DISCOVER', 'DISCOVER_DINERS', 'AMERICAN_EXPRESS'].includes(estimate?.availablePaymentMethods?.cardOnFile?.card?.brand)) {
 | 
			
		||||
                      <app-svg-images [name]="estimate?.availablePaymentMethods?.cardOnFile?.card?.brand" height="33" class="mr-2"></app-svg-images>
 | 
			
		||||
                    } @else {
 | 
			
		||||
                      <app-svg-images name="OTHER_BRAND" height="33" class="mr-2"></app-svg-images>
 | 
			
		||||
                    }
 | 
			
		||||
                    <span style="font-size: 22px; padding-bottom: 3px">{{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }}</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                }
 | 
			
		||||
              </div>
 | 
			
		||||
            }
 | 
			
		||||
          </div>
 | 
			
		||||
@ -457,7 +435,7 @@
 | 
			
		||||
        <button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('summary')" i18n="go-back">Go back</button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  } @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay' || step === 'cardonfile') {
 | 
			
		||||
  } @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay') {
 | 
			
		||||
    <!-- Show checkout page -->
 | 
			
		||||
    <div class="row mb-md-1 text-center" id="confirm-title">
 | 
			
		||||
      <div class="col-sm" id="confirm-payment-title">
 | 
			
		||||
@ -473,7 +451,7 @@
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    @if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay || step === 'cardonfile' && !loadingCardOnFile) {
 | 
			
		||||
    @if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay) {
 | 
			
		||||
      <div class="row text-center mt-1">
 | 
			
		||||
        <div class="col-sm">
 | 
			
		||||
          <div class="form-group w-100">
 | 
			
		||||
@ -498,24 +476,14 @@
 | 
			
		||||
            <div id="cash-app-pay" class="d-inline-block" style="height: 50px" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
 | 
			
		||||
          } @else if (step === 'googlepay') {
 | 
			
		||||
            <div id="google-pay-button" class="d-inline-block" style="height: 50px" [style]="loadingGooglePay ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
 | 
			
		||||
          } @else if (step === 'cardonfile') {
 | 
			
		||||
            <div class="paymentMethod mx-2 d-flex justify-content-center align-items-center ml-auto mr-auto" style="width: 200px; height: 55px" (click)="requestCardOnFilePayment()" [style]="loadingCardOnFile ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''">
 | 
			
		||||
              <fa-icon style="font-size: 24px; color: white" [icon]="['fas', 'credit-card']"></fa-icon>
 | 
			
		||||
              <span class="ml-2" style="font-size: 22px">{{ estimate?.availablePaymentMethods?.cardOnFile?.card?.brand }} {{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }}</span>
 | 
			
		||||
            </div>
 | 
			
		||||
          }
 | 
			
		||||
          @if (loadingCashapp || loadingApplePay || loadingGooglePay || loadingCardOnFile) {
 | 
			
		||||
          @if (loadingCashapp || loadingApplePay || loadingGooglePay) {
 | 
			
		||||
          <div display="d-flex flex-row justify-content-center">
 | 
			
		||||
            <span i18n="accelerator.loading-payment-method">Loading payment method...</span>
 | 
			
		||||
            <div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></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>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -8,13 +8,6 @@
 | 
			
		||||
  color: var(--green)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.accelerate-checkout-inner {
 | 
			
		||||
  &.input-disabled {
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
    opacity: 0.75;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.paymentMethod {
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
  background-color: var(--secondary);
 | 
			
		||||
@ -153,11 +146,6 @@
 | 
			
		||||
 | 
			
		||||
.payment-area {
 | 
			
		||||
  background: var(--bg);
 | 
			
		||||
  margin-top: 0.5rem;
 | 
			
		||||
  padding: 0.5rem;
 | 
			
		||||
  @media (max-width: 575px) {
 | 
			
		||||
    padding-bottom: 1.25rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.col.pie {
 | 
			
		||||
@ -224,17 +212,4 @@
 | 
			
		||||
}
 | 
			
		||||
.apple-pay-button-white-with-line {
 | 
			
		||||
    -apple-pay-button-style: white-outline;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btcpay-invoice {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  height: 292px;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  @media (max-width: 575px) {
 | 
			
		||||
    height: 75px;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    gap: 5px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -13,7 +13,7 @@ import { EnterpriseService } from '@app/services/enterprise.service';
 | 
			
		||||
import { ApiService } from '@app/services/api.service';
 | 
			
		||||
import { isDevMode } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay' | 'cardOnFile';
 | 
			
		||||
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay';
 | 
			
		||||
 | 
			
		||||
export type AccelerationEstimate = {
 | 
			
		||||
  hasAccess: boolean;
 | 
			
		||||
@ -26,7 +26,7 @@ export type AccelerationEstimate = {
 | 
			
		||||
  mempoolBaseFee: number;
 | 
			
		||||
  vsizeFee: number;
 | 
			
		||||
  pools: number[];
 | 
			
		||||
  availablePaymentMethods: Record<PaymentMethod, {min: number, max: number, card?: {card_id: string, last_4: string, brand: string, name: string, billing: any}}>;
 | 
			
		||||
  availablePaymentMethods: Record<PaymentMethod, {min: number, max: number}>;
 | 
			
		||||
  unavailable?: boolean;
 | 
			
		||||
  options: { // recommended bid options
 | 
			
		||||
    fee: number; // recommended userBid in sats
 | 
			
		||||
@ -49,7 +49,7 @@ export const MIN_BID_RATIO = 1;
 | 
			
		||||
export const DEFAULT_BID_RATIO = 2;
 | 
			
		||||
export const MAX_BID_RATIO = 4;
 | 
			
		||||
 | 
			
		||||
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'cardonfile' | 'processing' | 'paid' | 'success';
 | 
			
		||||
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'processing' | 'paid' | 'success';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-accelerate-checkout',
 | 
			
		||||
@ -62,9 +62,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
			
		||||
  @Input() miningStats: MiningStats;
 | 
			
		||||
  @Input() eta: ETA;
 | 
			
		||||
  @Input() scrollEvent: boolean;
 | 
			
		||||
  @Input() cashappEnabled: boolean = true;
 | 
			
		||||
  @Input() applePayEnabled: boolean = false;
 | 
			
		||||
  @Input() googlePayEnabled: boolean = true;
 | 
			
		||||
  @Input() cardOnFileEnabled: boolean = true;
 | 
			
		||||
  @Input() advancedEnabled: boolean = false;
 | 
			
		||||
  @Input() forceMobile: boolean = false;
 | 
			
		||||
  @Input() showDetails: boolean = false;
 | 
			
		||||
@ -76,8 +76,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
  calculating = true;
 | 
			
		||||
  processing = false;
 | 
			
		||||
  isCheckoutLocked = 0; // reference counter, 0 = unlocked, >0 = locked
 | 
			
		||||
  isTokenizing = 0; // reference counter, 0 = false, >0 = true
 | 
			
		||||
  selectedOption: 'wait' | 'accel';
 | 
			
		||||
  cantPayReason = '';
 | 
			
		||||
  quoteError = ''; // error fetching estimate or initial data
 | 
			
		||||
@ -117,7 +115,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
			
		||||
  loadingCashapp = false;
 | 
			
		||||
  loadingApplePay = false;
 | 
			
		||||
  loadingGooglePay = false;
 | 
			
		||||
  loadingCardOnFile = false;
 | 
			
		||||
  payments: any;
 | 
			
		||||
  cashAppPay: any;
 | 
			
		||||
  applePay: any;
 | 
			
		||||
@ -157,7 +154,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
			
		||||
        this.accelerateError = null;
 | 
			
		||||
        this.timePaid = 0;
 | 
			
		||||
        this.btcpayInvoiceFailed = false;
 | 
			
		||||
        this.moveToStep('summary', true);
 | 
			
		||||
        this.moveToStep('summary');
 | 
			
		||||
      } else {
 | 
			
		||||
        this.auth = auth;
 | 
			
		||||
      }
 | 
			
		||||
@ -166,11 +163,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    const urlParams = new URLSearchParams(window.location.search);
 | 
			
		||||
    if (urlParams.get('cash_request_id')) { // Redirected from cashapp
 | 
			
		||||
      this.moveToStep('processing', true);
 | 
			
		||||
      this.moveToStep('processing');
 | 
			
		||||
      this.insertSquare();
 | 
			
		||||
      this.setupSquare();
 | 
			
		||||
    } else {
 | 
			
		||||
      this.moveToStep('summary', true);
 | 
			
		||||
      this.moveToStep('summary');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.conversionsSubscription = this.stateService.conversions$.subscribe(
 | 
			
		||||
@ -195,23 +192,20 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
			
		||||
    }
 | 
			
		||||
    if (changes.accelerating && this.accelerating) {
 | 
			
		||||
      if (this.step === 'processing' || this.step === 'paid') {
 | 
			
		||||
        this.moveToStep('success', true);
 | 
			
		||||
        this.moveToStep('success');
 | 
			
		||||
      } else { // Edge case where the transaction gets accelerated by someone else or on another session
 | 
			
		||||
        this.closeModal();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  moveToStep(step: CheckoutStep, force: boolean = false): void {
 | 
			
		||||
    if (this.isCheckoutLocked > 0 && !force) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  moveToStep(step: CheckoutStep): void {
 | 
			
		||||
    this.processing = false;
 | 
			
		||||
    this._step = step;
 | 
			
		||||
    if (this.timeoutTimer) {
 | 
			
		||||
      clearTimeout(this.timeoutTimer);
 | 
			
		||||
    }
 | 
			
		||||
    if (!this.estimate && ['quote', 'summary', 'checkout', 'processing'].includes(this.step)) {
 | 
			
		||||
    if (!this.estimate && ['quote', 'summary', 'checkout'].includes(this.step)) {
 | 
			
		||||
      this.fetchEstimate();
 | 
			
		||||
    }
 | 
			
		||||
    if (this._step === 'checkout') {
 | 
			
		||||
@ -220,9 +214,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
			
		||||
    }
 | 
			
		||||
    if (this._step === 'checkout' && this.canPayWithBitcoin) {
 | 
			
		||||
      this.btcpayInvoiceFailed = false;
 | 
			
		||||
      this.loadingBtcpayInvoice = true;
 | 
			
		||||
      this.invoice = null;
 | 
			
		||||
      this.requestBTCPayInvoice();
 | 
			
		||||
    } else if (this._step === 'cashapp') {
 | 
			
		||||
    } else if (this._step === 'cashapp' && this.cashappEnabled) {
 | 
			
		||||
      this.loadingCashapp = true;
 | 
			
		||||
      this.setupSquare();
 | 
			
		||||
      this.scrollToElementWithTimeout('confirm-title', 'center', 100);
 | 
			
		||||
@ -234,10 +229,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
			
		||||
      this.loadingGooglePay = true;
 | 
			
		||||
      this.setupSquare();
 | 
			
		||||
      this.scrollToElementWithTimeout('confirm-title', 'center', 100);
 | 
			
		||||
    } else if (this._step === 'cardonfile' && this.cardOnFileEnabled) {
 | 
			
		||||
      this.loadingCardOnFile = true;
 | 
			
		||||
      this.setupSquare();
 | 
			
		||||
      this.scrollToElementWithTimeout('confirm-title', 'center', 100);
 | 
			
		||||
    } else if (this._step === 'paid') {
 | 
			
		||||
      this.timePaid = Date.now();
 | 
			
		||||
      this.timeoutTimer = setTimeout(() => {
 | 
			
		||||
@ -251,7 +242,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
  closeModal(): void {
 | 
			
		||||
    this.completed.emit(true);
 | 
			
		||||
    this.moveToStep('summary', true);
 | 
			
		||||
    this.moveToStep('summary');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -332,6 +323,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (this.step === 'checkout' && this.canPayWithBitcoin && !this.loadingBtcpayInvoice) {
 | 
			
		||||
            this.loadingBtcpayInvoice = true;
 | 
			
		||||
            this.requestBTCPayInvoice();
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
@ -401,7 +393,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
			
		||||
        this.audioService.playSound('ascend-chime-cartoon');
 | 
			
		||||
        this.showSuccess = true;
 | 
			
		||||
        this.estimateSubscription.unsubscribe();
 | 
			
		||||
        this.moveToStep('paid', true);
 | 
			
		||||
        this.moveToStep('paid');
 | 
			
		||||
      },
 | 
			
		||||
      error: (response) => {
 | 
			
		||||
        this.processing = false;
 | 
			
		||||
@ -457,8 +449,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
			
		||||
            await this.requestApplePayPayment();
 | 
			
		||||
          } else if (this._step === 'googlepay') {
 | 
			
		||||
            await this.requestGooglePayPayment();
 | 
			
		||||
          } else if (this._step === 'cardonfile') {
 | 
			
		||||
            this.loadingCardOnFile = false;
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        error: () => {
 | 
			
		||||
@ -513,75 +503,56 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
			
		||||
          }
 | 
			
		||||
          this.loadingApplePay = false;
 | 
			
		||||
          applePayButton.addEventListener('click', async event => {
 | 
			
		||||
            if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
 | 
			
		||||
              return;
 | 
			
		||||
            }
 | 
			
		||||
            event.preventDefault();
 | 
			
		||||
            try {
 | 
			
		||||
              // lock the checkout UI and show a loading spinner until the square modals are finished
 | 
			
		||||
              this.isCheckoutLocked++;
 | 
			
		||||
              this.isTokenizing++;
 | 
			
		||||
              const tokenResult = await this.applePay.tokenize();
 | 
			
		||||
              if (tokenResult?.status === 'OK') {
 | 
			
		||||
                const card = tokenResult.details?.card;
 | 
			
		||||
                if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
 | 
			
		||||
                  console.error(`Cannot retreive payment card details`);
 | 
			
		||||
                  this.accelerateError = 'apple_pay_no_card_details';
 | 
			
		||||
                  this.processing = false;
 | 
			
		||||
                  return;
 | 
			
		||||
                }
 | 
			
		||||
                const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
 | 
			
		||||
                // keep checkout in loading state until the acceleration request completes
 | 
			
		||||
                this.isTokenizing++;
 | 
			
		||||
                this.isCheckoutLocked++;
 | 
			
		||||
                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.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')}`, ``));
 | 
			
		||||
                      }, 10000);
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                });
 | 
			
		||||
              } else {
 | 
			
		||||
            const tokenResult = await this.applePay.tokenize();
 | 
			
		||||
            if (tokenResult?.status === 'OK') {
 | 
			
		||||
              const card = tokenResult.details?.card;
 | 
			
		||||
              if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
 | 
			
		||||
                console.error(`Cannot retreive payment card details`);
 | 
			
		||||
                this.accelerateError = 'apple_pay_no_card_details';
 | 
			
		||||
                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);
 | 
			
		||||
                return;
 | 
			
		||||
              }
 | 
			
		||||
            } finally {
 | 
			
		||||
              // always unlock the checkout once we're finished
 | 
			
		||||
              this.isTokenizing--;
 | 
			
		||||
              this.isCheckoutLocked--;
 | 
			
		||||
              const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
 | 
			
		||||
              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.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
 | 
			
		||||
                  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);
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              });
 | 
			
		||||
            } 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);
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
@ -631,193 +602,62 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
			
		||||
        this.loadingGooglePay = false;
 | 
			
		||||
 | 
			
		||||
        document.getElementById('google-pay-button').addEventListener('click', async event => {
 | 
			
		||||
          if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          event.preventDefault();
 | 
			
		||||
          try {
 | 
			
		||||
            // lock the checkout UI and show a loading spinner until the square modals are finished
 | 
			
		||||
            this.isCheckoutLocked++;
 | 
			
		||||
            this.isTokenizing++;
 | 
			
		||||
            const tokenResult = await this.googlePay.tokenize();
 | 
			
		||||
            if (tokenResult?.status === 'OK') {
 | 
			
		||||
              const card = tokenResult.details?.card;
 | 
			
		||||
              if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
 | 
			
		||||
                console.error(`Cannot retreive payment card details`);
 | 
			
		||||
                this.accelerateError = 'apple_pay_no_card_details';
 | 
			
		||||
                this.processing = false;
 | 
			
		||||
                return;
 | 
			
		||||
              }
 | 
			
		||||
              const verificationToken = await this.$verifyBuyer(this.payments, tokenResult.token, tokenResult.details, costUSD.toFixed(2));
 | 
			
		||||
              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());
 | 
			
		||||
              // 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')}`, ``));
 | 
			
		||||
                    }, 10000);
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              });
 | 
			
		||||
            } else {
 | 
			
		||||
          const tokenResult = await this.googlePay.tokenize();
 | 
			
		||||
          if (tokenResult?.status === 'OK') {
 | 
			
		||||
            const card = tokenResult.details?.card;
 | 
			
		||||
            if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
 | 
			
		||||
              console.error(`Cannot retreive payment card details`);
 | 
			
		||||
              this.accelerateError = 'apple_pay_no_card_details';
 | 
			
		||||
              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);
 | 
			
		||||
              return;
 | 
			
		||||
            }
 | 
			
		||||
          } finally {
 | 
			
		||||
            // always unlock the checkout once we're finished
 | 
			
		||||
            this.isTokenizing--;
 | 
			
		||||
            this.isCheckoutLocked--;
 | 
			
		||||
            const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
 | 
			
		||||
            this.servicesApiService.accelerateWithGooglePay$(
 | 
			
		||||
              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.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
 | 
			
		||||
                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);
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            });
 | 
			
		||||
          } 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);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Card On File
 | 
			
		||||
   */
 | 
			
		||||
  async requestCardOnFilePayment(): Promise<void> {
 | 
			
		||||
    if (this.processing) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (this.conversionsSubscription) {
 | 
			
		||||
      this.conversionsSubscription.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    this.processing = true;
 | 
			
		||||
    this.conversionsSubscription = this.stateService.conversions$.subscribe(
 | 
			
		||||
      async (conversions) => {
 | 
			
		||||
        this.conversions = conversions;
 | 
			
		||||
 | 
			
		||||
        const costUSD = this.cost / 100_000_000 * conversions.USD;
 | 
			
		||||
        if (this.isCheckoutLocked > 0) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        const cardOnFile = this.estimate?.availablePaymentMethods?.cardOnFile;
 | 
			
		||||
        if (!cardOnFile?.card) {
 | 
			
		||||
          this.accelerateError = 'card_on_file_not_found';
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        this.loadingCardOnFile = false;
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
          this.isCheckoutLocked += 2;
 | 
			
		||||
          this.isTokenizing += 2;
 | 
			
		||||
          
 | 
			
		||||
          const nameParts = cardOnFile.card.name.split(' ');
 | 
			
		||||
          const assumedGivenName = nameParts[0];
 | 
			
		||||
          const assumedFamilyName = nameParts.length > 1 ? nameParts[1] : undefined;
 | 
			
		||||
          const verificationDetails = {
 | 
			
		||||
            card: {
 | 
			
		||||
              billing: {
 | 
			
		||||
                givenName: assumedGivenName,
 | 
			
		||||
                familyName: assumedFamilyName,
 | 
			
		||||
                addressLines: [cardOnFile.card.billing.addressLine1 ?? ''],
 | 
			
		||||
                city: cardOnFile.card.billing.locality ?? '',
 | 
			
		||||
                state: cardOnFile.card.billing.administrativeDistrictLevel1 ?? '',
 | 
			
		||||
                countyCode: cardOnFile.card.billing.country,
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          };
 | 
			
		||||
          const verificationToken = await this.$verifyBuyer(this.payments, cardOnFile.card.card_id, verificationDetails, costUSD.toFixed(2));
 | 
			
		||||
          if (!verificationToken || !verificationToken.token) {
 | 
			
		||||
            console.error(`SCA verification failed`);
 | 
			
		||||
            this.accelerateError = 'SCA Verification Failed. Payment Declined.';
 | 
			
		||||
            this.processing = false;
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          this.servicesApiService.accelerateWithCardOnFile$(
 | 
			
		||||
            this.tx.txid,
 | 
			
		||||
            cardOnFile.card.card_id,
 | 
			
		||||
            verificationToken.token,
 | 
			
		||||
            `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');
 | 
			
		||||
              setTimeout(() => {
 | 
			
		||||
                this.isCheckoutLocked--;
 | 
			
		||||
                this.isTokenizing--;
 | 
			
		||||
                this.moveToStep('paid', true);
 | 
			
		||||
              }, 1000);
 | 
			
		||||
            },
 | 
			
		||||
            error: (response) => {
 | 
			
		||||
              this.processing = false;
 | 
			
		||||
              this.accelerateError = response.error;
 | 
			
		||||
              this.isCheckoutLocked--;
 | 
			
		||||
              this.isTokenizing--;
 | 
			
		||||
              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);
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          console.log(e);
 | 
			
		||||
          this.isCheckoutLocked--;
 | 
			
		||||
          this.isTokenizing--;
 | 
			
		||||
          this.processing = false;
 | 
			
		||||
          this.accelerateError = e.message;
 | 
			
		||||
 | 
			
		||||
        } finally {
 | 
			
		||||
          // always unlock the checkout once we're finished
 | 
			
		||||
          this.isCheckoutLocked--;
 | 
			
		||||
          this.isTokenizing--;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * CASHAPP
 | 
			
		||||
   */
 | 
			
		||||
@ -838,7 +678,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const redirectHostname = document.location.hostname === 'localhost' ? `http://localhost:4200`: `https://${document.location.hostname}`;
 | 
			
		||||
        const costUSD = this.cost / 100_000_000 * conversions.USD;
 | 
			
		||||
        const costUSD = this.step === 'processing' ? 69.69 : (this.cost / 100_000_000 * conversions.USD); // When we're redirected to this component, the payment data is already linked to the payment token, so does not matter what amonut we put in there, therefore it's 69.69
 | 
			
		||||
        const paymentRequest = this.payments.paymentRequest({
 | 
			
		||||
          countryCode: 'US',
 | 
			
		||||
          currencyCode: 'USD',
 | 
			
		||||
@ -878,7 +718,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
			
		||||
                  this.cashAppPay.destroy();
 | 
			
		||||
                }
 | 
			
		||||
                setTimeout(() => {
 | 
			
		||||
                  this.moveToStep('paid', true);
 | 
			
		||||
                  this.moveToStep('paid');
 | 
			
		||||
                  if (window.history.replaceState) {
 | 
			
		||||
                    const urlParams = new URLSearchParams(window.location.search);
 | 
			
		||||
                    window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ''));
 | 
			
		||||
@ -893,7 +733,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
			
		||||
                    // 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')}`, ``));
 | 
			
		||||
                  }, 10000);
 | 
			
		||||
                  }, 3000);
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            });
 | 
			
		||||
@ -903,49 +743,20 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * https://developer.squareup.com/docs/sca-overview
 | 
			
		||||
   */
 | 
			
		||||
  async $verifyBuyer(payments, token, details, amount): Promise<{token: string, userChallenged: boolean}> {
 | 
			
		||||
    const verificationDetails = {
 | 
			
		||||
      amount: amount,
 | 
			
		||||
      currencyCode: 'USD',
 | 
			
		||||
      intent: 'CHARGE',
 | 
			
		||||
      billingContact: {
 | 
			
		||||
        givenName: details.card?.billing?.givenName,
 | 
			
		||||
        familyName: details.card?.billing?.familyName,
 | 
			
		||||
        phone: details.card?.billing?.phone,
 | 
			
		||||
        addressLines: details.card?.billing?.addressLines,
 | 
			
		||||
        city: details.card?.billing?.city,
 | 
			
		||||
        state: details.card?.billing?.state,
 | 
			
		||||
        countryCode: details.card?.billing?.countryCode,
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const verificationResults = await payments.verifyBuyer(
 | 
			
		||||
      token,
 | 
			
		||||
      verificationDetails,
 | 
			
		||||
    );
 | 
			
		||||
    return verificationResults;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * BTCPay
 | 
			
		||||
   */
 | 
			
		||||
  async requestBTCPayInvoice(): Promise<void> {
 | 
			
		||||
    this.loadingBtcpayInvoice = true;
 | 
			
		||||
    this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe(
 | 
			
		||||
      switchMap(response => {
 | 
			
		||||
        return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId);
 | 
			
		||||
      }),
 | 
			
		||||
      catchError(error => {
 | 
			
		||||
        console.log(error);
 | 
			
		||||
        this.loadingBtcpayInvoice = false;
 | 
			
		||||
        this.btcpayInvoiceFailed = true;
 | 
			
		||||
        return of(null);
 | 
			
		||||
      })
 | 
			
		||||
    ).subscribe((invoice) => {
 | 
			
		||||
        this.loadingBtcpayInvoice = false;
 | 
			
		||||
        this.invoice = invoice;
 | 
			
		||||
        this.cd.markForCheck();
 | 
			
		||||
    });
 | 
			
		||||
@ -955,7 +766,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
			
		||||
    this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
 | 
			
		||||
    this.audioService.playSound('ascend-chime-cartoon');
 | 
			
		||||
    this.estimateSubscription.unsubscribe();
 | 
			
		||||
    this.moveToStep('paid', true);
 | 
			
		||||
    this.moveToStep('paid');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isLoggedIn(): boolean {
 | 
			
		||||
@ -982,6 +793,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get couldPayWithCashapp(): boolean {
 | 
			
		||||
    if (!this.cashappEnabled) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    return !!this.estimate?.availablePaymentMethods?.cashapp;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -1016,7 +830,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get canPayWithCashapp(): boolean {
 | 
			
		||||
    if (!this.conversions || (!this.isProdDomain && !isDevMode())) {
 | 
			
		||||
    if (!this.cashappEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1063,22 +877,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get canPayWithCardOnFile(): boolean {
 | 
			
		||||
    if (!this.cardOnFileEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const paymentMethod = this.estimate?.availablePaymentMethods?.cardOnFile;
 | 
			
		||||
    if (paymentMethod) {
 | 
			
		||||
      const costUSD = (this.cost / 100_000_000 * this.conversions.USD);
 | 
			
		||||
      if (costUSD >= paymentMethod.min && costUSD <= paymentMethod.max) {
 | 
			
		||||
        return true;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get canPayWithBalance(): boolean {
 | 
			
		||||
    if (!this.hasAccessToBalanceMode) {
 | 
			
		||||
      return false;
 | 
			
		||||
 | 
			
		||||
@ -46,8 +46,6 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
 | 
			
		||||
 | 
			
		||||
  aggregatedHistory$: Observable<any>;
 | 
			
		||||
  statsSubscription: Subscription;
 | 
			
		||||
  aggregatedHistorySubscription: Subscription;
 | 
			
		||||
  fragmentSubscription: Subscription;
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
  formatNumber = formatNumber;
 | 
			
		||||
  timespan = '';
 | 
			
		||||
@ -81,8 +79,8 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
 | 
			
		||||
    }
 | 
			
		||||
    this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
 | 
			
		||||
    this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
 | 
			
		||||
 | 
			
		||||
    this.fragmentSubscription = this.route.fragment.subscribe((fragment) => {
 | 
			
		||||
    
 | 
			
		||||
    this.route.fragment.subscribe((fragment) => {
 | 
			
		||||
      if (['24h', '3d', '1w', '1m', '3m', 'all'].indexOf(fragment) > -1) {
 | 
			
		||||
        this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
 | 
			
		||||
      }
 | 
			
		||||
@ -115,7 +113,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
 | 
			
		||||
      share(),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.aggregatedHistorySubscription = this.aggregatedHistory$.subscribe();
 | 
			
		||||
    this.aggregatedHistory$.subscribe();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges(changes: SimpleChanges): void {
 | 
			
		||||
@ -337,8 +335,8 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    this.aggregatedHistorySubscription?.unsubscribe();
 | 
			
		||||
    this.fragmentSubscription?.unsubscribe();
 | 
			
		||||
    this.statsSubscription?.unsubscribe();
 | 
			
		||||
    if (this.statsSubscription) {
 | 
			
		||||
      this.statsSubscription.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@
 | 
			
		||||
          <th class="time text-right" i18n="accelerator.requested">Requested</th>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
        <ng-container *ngIf="!pending">
 | 
			
		||||
          <th class="fee text-right text-truncate" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th>
 | 
			
		||||
          <th class="fee text-right" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th>
 | 
			
		||||
          <th class="block text-right" i18n="shared.block-title">Block</th>
 | 
			
		||||
          <th class="pool text-right" i18n="mining.pool-name" *ngIf="!this.widget">Pool</th>
 | 
			
		||||
          <th class="status text-right" i18n="transaction.status|Transaction Status">Status</th>
 | 
			
		||||
@ -64,8 +64,7 @@
 | 
			
		||||
              <span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span>
 | 
			
		||||
              <span *ngIf="acceleration.status.includes('completed') && acceleration.minedByPoolUniqueId && pools[acceleration.minedByPoolUniqueId]" class="badge badge-success"><ng-container i18n="accelerator.completed">Completed</ng-container><span *ngIf="acceleration.status === 'completed_provisional'"> ⌛</span></span>
 | 
			
		||||
              <span *ngIf="acceleration.status.includes('completed') && (!acceleration.minedByPoolUniqueId || !pools[acceleration.minedByPoolUniqueId])" class="badge badge-success"><ng-container i18n="transaction.rbf.mined">Mined</ng-container><span *ngIf="acceleration.status === 'completed_provisional'"> ⌛</span></span>
 | 
			
		||||
              <span *ngIf="acceleration.status.includes('failed') && acceleration.canceled" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Canceled</ng-container><span *ngIf="acceleration.status === 'failed_provisional'"> ⌛</span></span>
 | 
			
		||||
              <span *ngIf="acceleration.status.includes('failed') && !acceleration.canceled" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Failed</ng-container><span *ngIf="acceleration.status === 'failed_provisional'"> ⌛</span></span>
 | 
			
		||||
              <span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Canceled</ng-container><span *ngIf="acceleration.status === 'failed_provisional'"> ⌛</span></span>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="date text-right" *ngIf="!this.widget">
 | 
			
		||||
              <app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
 | 
			
		||||
 | 
			
		||||
@ -123,7 +123,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
 | 
			
		||||
                    } else if (this.conversions && this.conversions['USD']) {
 | 
			
		||||
                      price = this.conversions['USD'];
 | 
			
		||||
                    }
 | 
			
		||||
                    return { ...item, price: price };
 | 
			
		||||
                    return { ...item, price: price }
 | 
			
		||||
                  });
 | 
			
		||||
                }
 | 
			
		||||
              }),
 | 
			
		||||
@ -350,7 +350,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
 | 
			
		||||
            show: this.showYAxis,
 | 
			
		||||
            color: 'rgb(110, 112, 121)',
 | 
			
		||||
            formatter: function(val) {
 | 
			
		||||
              return `$${this.amountShortenerPipe.transform(val, 3, undefined, true, true)}`;
 | 
			
		||||
              return `$${this.amountShortenerPipe.transform(val, 0, undefined, true)}`;
 | 
			
		||||
            }.bind(this)
 | 
			
		||||
          },
 | 
			
		||||
          splitLine: {
 | 
			
		||||
@ -418,7 +418,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
 | 
			
		||||
 | 
			
		||||
  onChartClick(e) {
 | 
			
		||||
    if (this.hoverData?.length && this.hoverData[0]?.[2]?.txid) {
 | 
			
		||||
      this.zone.run(() => {
 | 
			
		||||
      this.zone.run(() => { 
 | 
			
		||||
        const url = this.relativeUrlPipe.transform(`/tx/${this.hoverData[0][2].txid}`);
 | 
			
		||||
        if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) {
 | 
			
		||||
          window.open(url);
 | 
			
		||||
@ -478,30 +478,25 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  extendSummary(summary) {
 | 
			
		||||
    const extendedSummary = summary.slice();
 | 
			
		||||
    let extendedSummary = summary.slice();
 | 
			
		||||
 | 
			
		||||
    // Add a point at today's date to make the graph end at the current time
 | 
			
		||||
    extendedSummary.unshift({ time: Date.now() / 1000, value: 0 });
 | 
			
		||||
 | 
			
		||||
    let maxTime = Date.now() / 1000;
 | 
			
		||||
 | 
			
		||||
    const oneHour = 60 * 60;
 | 
			
		||||
    extendedSummary.reverse();
 | 
			
		||||
    
 | 
			
		||||
    let oneHour = 60 * 60;
 | 
			
		||||
    // Fill gaps longer than interval
 | 
			
		||||
    for (let i = 0; i < extendedSummary.length - 1; i++) {
 | 
			
		||||
      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);
 | 
			
		||||
      let hours = Math.floor((extendedSummary[i + 1].time - extendedSummary[i].time) / oneHour);      
 | 
			
		||||
      if (hours > 1) {
 | 
			
		||||
        for (let j = 1; j < hours; j++) {
 | 
			
		||||
          const newTime = extendedSummary[i].time - oneHour * j;
 | 
			
		||||
          let newTime = extendedSummary[i].time + oneHour * j;
 | 
			
		||||
          extendedSummary.splice(i + j, 0, { time: newTime, value: 0 });
 | 
			
		||||
        }
 | 
			
		||||
        i += hours - 1;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return extendedSummary;
 | 
			
		||||
  
 | 
			
		||||
    return extendedSummary.reverse();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -41,7 +41,7 @@ export class AppComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  @HostListener('document:keydown', ['$event'])
 | 
			
		||||
  handleKeyboardEvents(event: KeyboardEvent) {
 | 
			
		||||
    if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
 | 
			
		||||
    if (event.target instanceof HTMLInputElement) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    // prevent arrow key horizontal scrolling
 | 
			
		||||
 | 
			
		||||
@ -10,10 +10,6 @@
 | 
			
		||||
    </span>
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  <div class="d-flex justify-content-center">
 | 
			
		||||
    <app-mempool-error *ngIf="paymentErrorMessage" [error]="paymentErrorMessage"></app-mempool-error>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div *ngIf="paymentStatus === 2">
 | 
			
		||||
    
 | 
			
		||||
    <form [formGroup]="paymentForm">
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,9 @@
 | 
			
		||||
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core';
 | 
			
		||||
import { FormBuilder, FormGroup } from '@angular/forms';
 | 
			
		||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
 | 
			
		||||
import { Subscription, of, catchError } from 'rxjs';
 | 
			
		||||
import { retry, tap } from 'rxjs/operators';
 | 
			
		||||
import { ActivatedRoute } from '@angular/router';
 | 
			
		||||
import { Subscription, of, timer } from 'rxjs';
 | 
			
		||||
import { filter, repeat, retry, switchMap, take, tap } from 'rxjs/operators';
 | 
			
		||||
import { ServicesApiServices } from '@app/services/services-api.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
@ -17,17 +18,30 @@ export class BitcoinInvoiceComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
  @Output() completed = new EventEmitter();
 | 
			
		||||
 | 
			
		||||
  paymentForm: FormGroup;
 | 
			
		||||
  requestSubscription: Subscription | undefined;
 | 
			
		||||
  paymentStatusSubscription: Subscription | undefined;
 | 
			
		||||
  paymentStatus = 1; // 1 - Waiting for invoice | 2 - Pending payment | 3 - Payment completed
 | 
			
		||||
  paymentErrorMessage: string = '';
 | 
			
		||||
  paramMapSubscription: Subscription | undefined;
 | 
			
		||||
  invoiceSubscription: Subscription | undefined;
 | 
			
		||||
  invoiceTimeout; // Wait for angular to load all the things before making a request
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private formBuilder: FormBuilder,
 | 
			
		||||
    private apiService: ServicesApiServices,
 | 
			
		||||
    private sanitizer: DomSanitizer
 | 
			
		||||
    private sanitizer: DomSanitizer,
 | 
			
		||||
    private activatedRoute: ActivatedRoute
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy() {
 | 
			
		||||
    if (this.requestSubscription) {
 | 
			
		||||
      this.requestSubscription.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
    if (this.paramMapSubscription) {
 | 
			
		||||
      this.paramMapSubscription.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
    if (this.invoiceSubscription) {
 | 
			
		||||
      this.invoiceSubscription.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
    if (this.paymentStatusSubscription) {
 | 
			
		||||
      this.paymentStatusSubscription.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
@ -58,39 +72,15 @@ export class BitcoinInvoiceComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
    } else {
 | 
			
		||||
      this.paymentStatus = 4;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.monitorPendingInvoice();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  monitorPendingInvoice(): void {
 | 
			
		||||
    if (!this.invoice) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (this.paymentStatusSubscription) {
 | 
			
		||||
      this.paymentStatusSubscription.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
    this.paymentStatusSubscription = this.apiService.getPaymentStatus$(this.invoice.btcpayInvoiceId).pipe(
 | 
			
		||||
      tap(result => {
 | 
			
		||||
        if (result.status === 204) { // Manually trigger an error in that case so we can retry
 | 
			
		||||
          throw result;
 | 
			
		||||
        } else if (result.status === 200) { // Invoice settled
 | 
			
		||||
          this.paymentStatus = 3;
 | 
			
		||||
          this.completed.emit();
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
      catchError(err => {
 | 
			
		||||
        if (err.status === 204 || err.status === 504) {
 | 
			
		||||
          throw err; // Will trigger the retry
 | 
			
		||||
        } else if (err.status === 400) {
 | 
			
		||||
          this.paymentErrorMessage = 'Invoice has expired';
 | 
			
		||||
        } else if (err.status === 404) {
 | 
			
		||||
          this.paymentErrorMessage = 'Invoice is no longer valid';
 | 
			
		||||
        }
 | 
			
		||||
        this.paymentStatus = -1;
 | 
			
		||||
        return of(null);
 | 
			
		||||
      }),
 | 
			
		||||
      retry({ delay: 1000 }),
 | 
			
		||||
    ).subscribe();
 | 
			
		||||
      retry({ delay: () => timer(2000)}),
 | 
			
		||||
      repeat({delay: 2000}),
 | 
			
		||||
      filter((response) => response.status !== 204 && response.status !== 404),
 | 
			
		||||
      take(1),
 | 
			
		||||
    ).subscribe(() => {
 | 
			
		||||
      this.paymentStatus = 3;
 | 
			
		||||
      this.completed.emit();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get availableMethods(): string[] {
 | 
			
		||||
 | 
			
		||||
@ -172,19 +172,13 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    if (this.animationFrameRequest) {
 | 
			
		||||
      cancelAnimationFrame(this.animationFrameRequest);
 | 
			
		||||
      clearTimeout(this.animationHeartBeat);
 | 
			
		||||
    }
 | 
			
		||||
    clearTimeout(this.animationHeartBeat);
 | 
			
		||||
    if (this.canvas) {
 | 
			
		||||
      this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost);
 | 
			
		||||
      this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored);
 | 
			
		||||
      this.themeChangedSubscription?.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
    if (this.scene) {
 | 
			
		||||
      this.scene.destroy();
 | 
			
		||||
    }
 | 
			
		||||
    this.vertexArray.destroy();
 | 
			
		||||
    this.vertexArray = null;
 | 
			
		||||
    this.themeChangedSubscription?.unsubscribe();
 | 
			
		||||
    this.searchSubscription?.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  clear(direction): void {
 | 
			
		||||
@ -453,7 +447,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
    }
 | 
			
		||||
    this.applyQueuedUpdates();
 | 
			
		||||
    // skip re-render if there's no change to the scene
 | 
			
		||||
    if (this.scene && this.gl && this.vertexArray) {
 | 
			
		||||
    if (this.scene && this.gl) {
 | 
			
		||||
      /* SET UP SHADER UNIFORMS */
 | 
			
		||||
      // screen dimensions
 | 
			
		||||
      this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight);
 | 
			
		||||
@ -495,7 +489,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
    if (this.running && this.scene && now <= (this.scene.animateUntil + 500)) {
 | 
			
		||||
      this.doRun();
 | 
			
		||||
    } else {
 | 
			
		||||
      clearTimeout(this.animationHeartBeat);
 | 
			
		||||
      if (this.animationHeartBeat) {
 | 
			
		||||
        clearTimeout(this.animationHeartBeat);
 | 
			
		||||
      }
 | 
			
		||||
      this.animationHeartBeat = window.setTimeout(() => {
 | 
			
		||||
        this.start();
 | 
			
		||||
      }, 1000);
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,6 @@ export class FastVertexArray {
 | 
			
		||||
  freeSlots: number[];
 | 
			
		||||
  lastSlot: number;
 | 
			
		||||
  dirty = false;
 | 
			
		||||
  destroyed = false;
 | 
			
		||||
 | 
			
		||||
  constructor(length, stride) {
 | 
			
		||||
    this.length = length;
 | 
			
		||||
@ -33,9 +32,6 @@ export class FastVertexArray {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  insert(sprite: TxSprite): number {
 | 
			
		||||
    if (this.destroyed) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.count++;
 | 
			
		||||
 | 
			
		||||
    let position;
 | 
			
		||||
@ -49,14 +45,11 @@ export class FastVertexArray {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    this.sprites[position] = sprite;
 | 
			
		||||
    this.dirty = true;
 | 
			
		||||
    return position;
 | 
			
		||||
    this.dirty = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  remove(index: number): void {
 | 
			
		||||
    if (this.destroyed) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.count--;
 | 
			
		||||
    this.clearData(index);
 | 
			
		||||
    this.freeSlots.push(index);
 | 
			
		||||
@ -68,26 +61,20 @@ export class FastVertexArray {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setData(index: number, dataChunk: number[]): void {
 | 
			
		||||
    if (this.destroyed) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.data.set(dataChunk, (index * this.stride));
 | 
			
		||||
    this.dirty = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private clearData(index: number): void {
 | 
			
		||||
  clearData(index: number): void {
 | 
			
		||||
    this.data.fill(0, (index * this.stride), ((index + 1) * this.stride));
 | 
			
		||||
    this.dirty = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getData(index: number): Float32Array {
 | 
			
		||||
    if (this.destroyed) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    return this.data.subarray(index, this.stride);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private expand(): void {
 | 
			
		||||
  expand(): void {
 | 
			
		||||
    this.length *= 2;
 | 
			
		||||
    const newData = new Float32Array(this.length * this.stride);
 | 
			
		||||
    newData.set(this.data);
 | 
			
		||||
@ -95,7 +82,7 @@ export class FastVertexArray {
 | 
			
		||||
    this.dirty = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private compact(): void {
 | 
			
		||||
  compact(): void {
 | 
			
		||||
    // New array length is the smallest power of 2 larger than the sprite count (but no smaller than 512)
 | 
			
		||||
    const newLength = Math.max(512, Math.pow(2, Math.ceil(Math.log2(this.count))));
 | 
			
		||||
    if (newLength !== this.length) {
 | 
			
		||||
@ -123,13 +110,4 @@ export class FastVertexArray {
 | 
			
		||||
  getVertexData(): Float32Array {
 | 
			
		||||
    return this.data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  destroy(): void {
 | 
			
		||||
    this.data = null;
 | 
			
		||||
    this.sprites = null;
 | 
			
		||||
    this.freeSlots = null;
 | 
			
		||||
    this.lastSlot = 0;
 | 
			
		||||
    this.dirty = false;
 | 
			
		||||
    this.destroyed = true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -116,7 +116,7 @@ export class BlockViewComponent implements OnInit, OnDestroy {
 | 
			
		||||
        this.isLoadingBlock = false;
 | 
			
		||||
        this.isLoadingOverview = true;
 | 
			
		||||
      }),
 | 
			
		||||
      shareReplay({ bufferSize: 1, refCount: true })
 | 
			
		||||
      shareReplay(1)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.overviewSubscription = block$.pipe(
 | 
			
		||||
@ -176,8 +176,5 @@ export class BlockViewComponent implements OnInit, OnDestroy {
 | 
			
		||||
    if (this.queryParamsSubscription) {
 | 
			
		||||
      this.queryParamsSubscription.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
    if (this.blockGraph) {
 | 
			
		||||
      this.blockGraph.destroy();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -117,7 +117,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
 | 
			
		||||
        this.openGraphService.waitOver('block-data-' + this.rawId);
 | 
			
		||||
      }),
 | 
			
		||||
      throttleTime(50, asyncScheduler, { leading: true, trailing: true }),
 | 
			
		||||
      shareReplay({ bufferSize: 1, refCount: true })
 | 
			
		||||
      shareReplay(1)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.overviewSubscription = block$.pipe(
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import { Component, OnInit, OnDestroy, ViewChildren, QueryList, ChangeDetectorRef } from '@angular/core';
 | 
			
		||||
import { Location } from '@angular/common';
 | 
			
		||||
import { ActivatedRoute, ParamMap, Params, Router } from '@angular/router';
 | 
			
		||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
 | 
			
		||||
import { ElectrsApiService } from '@app/services/electrs-api.service';
 | 
			
		||||
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, filter, take } from 'rxjs/operators';
 | 
			
		||||
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, filter } from 'rxjs/operators';
 | 
			
		||||
import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest, forkJoin } from 'rxjs';
 | 
			
		||||
import { StateService } from '@app/services/state.service';
 | 
			
		||||
import { SeoService } from '@app/services/seo.service';
 | 
			
		||||
@ -68,7 +68,6 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
  paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
 | 
			
		||||
  numUnexpected: number = 0;
 | 
			
		||||
  mode: 'projected' | 'actual' = 'projected';
 | 
			
		||||
  currentQueryParams: Params;
 | 
			
		||||
 | 
			
		||||
  overviewSubscription: Subscription;
 | 
			
		||||
  accelerationsSubscription: Subscription;
 | 
			
		||||
@ -81,8 +80,8 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
  timeLtr: boolean;
 | 
			
		||||
  childChangeSubscription: Subscription;
 | 
			
		||||
  auditPrefSubscription: Subscription;
 | 
			
		||||
  isAuditEnabledSubscription: Subscription;
 | 
			
		||||
  oobSubscription: Subscription;
 | 
			
		||||
  
 | 
			
		||||
  priceSubscription: Subscription;
 | 
			
		||||
  blockConversion: Price;
 | 
			
		||||
 | 
			
		||||
@ -119,7 +118,7 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
    this.setAuditAvailable(this.auditSupported);
 | 
			
		||||
 | 
			
		||||
    if (this.auditSupported) {
 | 
			
		||||
      this.isAuditEnabledSubscription = this.isAuditEnabledFromParam().subscribe(auditParam => {
 | 
			
		||||
      this.isAuditEnabledFromParam().subscribe(auditParam => {
 | 
			
		||||
        if (this.auditParamEnabled) {
 | 
			
		||||
          this.auditModeEnabled = auditParam;
 | 
			
		||||
        } else {
 | 
			
		||||
@ -282,7 +281,7 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
      throttleTime(300, asyncScheduler, { leading: true, trailing: true }),
 | 
			
		||||
      shareReplay({ bufferSize: 1, refCount: true })
 | 
			
		||||
      shareReplay(1)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.overviewSubscription = this.block$.pipe(
 | 
			
		||||
@ -364,7 +363,6 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
      .subscribe((network) => this.network = network);
 | 
			
		||||
 | 
			
		||||
    this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
 | 
			
		||||
      this.currentQueryParams = params;
 | 
			
		||||
      if (params.showDetails === 'true') {
 | 
			
		||||
        this.showDetails = true;
 | 
			
		||||
      } else {
 | 
			
		||||
@ -416,7 +414,6 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    this.stateService.markBlock$.next({});
 | 
			
		||||
    this.overviewSubscription?.unsubscribe();
 | 
			
		||||
    this.accelerationsSubscription?.unsubscribe();
 | 
			
		||||
    this.keyNavigationSubscription?.unsubscribe();
 | 
			
		||||
    this.blocksSubscription?.unsubscribe();
 | 
			
		||||
    this.cacheBlocksSubscription?.unsubscribe();
 | 
			
		||||
@ -424,16 +421,8 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
    this.queryParamsSubscription?.unsubscribe();
 | 
			
		||||
    this.timeLtrSubscription?.unsubscribe();
 | 
			
		||||
    this.childChangeSubscription?.unsubscribe();
 | 
			
		||||
    this.auditPrefSubscription?.unsubscribe();
 | 
			
		||||
    this.isAuditEnabledSubscription?.unsubscribe();
 | 
			
		||||
    this.oobSubscription?.unsubscribe();
 | 
			
		||||
    this.priceSubscription?.unsubscribe();
 | 
			
		||||
    this.blockGraphProjected.forEach(graph => {
 | 
			
		||||
      graph.destroy();
 | 
			
		||||
    });
 | 
			
		||||
    this.blockGraphActual.forEach(graph => {
 | 
			
		||||
      graph.destroy();
 | 
			
		||||
    });
 | 
			
		||||
    this.oobSubscription?.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // TODO - Refactor this.fees/this.reward for liquid because it is not
 | 
			
		||||
@ -744,18 +733,19 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
  toggleAuditMode(): void {
 | 
			
		||||
    this.stateService.hideAudit.next(this.auditModeEnabled);
 | 
			
		||||
 | 
			
		||||
    const queryParams = { ...this.currentQueryParams };
 | 
			
		||||
    delete queryParams['audit'];
 | 
			
		||||
    this.route.queryParams.subscribe(params => {
 | 
			
		||||
      const queryParams = { ...params };
 | 
			
		||||
      delete queryParams['audit'];
 | 
			
		||||
 | 
			
		||||
    let newUrl = this.router.url.split('?')[0];
 | 
			
		||||
    const queryString = new URLSearchParams(queryParams).toString();
 | 
			
		||||
    if (queryString) {
 | 
			
		||||
      newUrl += '?' + queryString;
 | 
			
		||||
    }
 | 
			
		||||
    this.location.replaceState(newUrl);
 | 
			
		||||
      let newUrl = this.router.url.split('?')[0];
 | 
			
		||||
      const queryString = new URLSearchParams(queryParams).toString();
 | 
			
		||||
      if (queryString) {
 | 
			
		||||
        newUrl += '?' + queryString;
 | 
			
		||||
      }
 | 
			
		||||
  
 | 
			
		||||
      this.location.replaceState(newUrl);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // avoid duplicate subscriptions
 | 
			
		||||
    this.auditPrefSubscription?.unsubscribe();
 | 
			
		||||
    this.auditPrefSubscription = this.stateService.hideAudit.subscribe((hide) => {
 | 
			
		||||
      this.auditModeEnabled = !hide;
 | 
			
		||||
      this.showAudit = this.auditAvailable && this.auditModeEnabled;
 | 
			
		||||
@ -772,7 +762,7 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
    return this.route.queryParams.pipe(
 | 
			
		||||
      map(params => {
 | 
			
		||||
        this.auditParamEnabled = 'audit' in params;
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        return this.auditParamEnabled ? !(params['audit'] === 'false') : true;
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
@ -49,7 +49,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="timestamp" *ngIf="!widget" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
 | 
			
		||||
            <app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="block.timestamp" [hideTimeSince]="true"></app-timestamp>
 | 
			
		||||
            ‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
 | 
			
		||||
          </td>
 | 
			
		||||
          <td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
 | 
			
		||||
            <a
 | 
			
		||||
 | 
			
		||||
@ -281,11 +281,9 @@
 | 
			
		||||
          <div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
              <div class="card-body">
 | 
			
		||||
                <a class="title-link mb-0" style="margin-top: -2px" href="" [routerLink]="['/wallet/' + widget.props.wallet | relativeUrl]">
 | 
			
		||||
                <span class="title-link">
 | 
			
		||||
                  <h5 class="card-title d-inline" i18n="dashboard.treasury-transactions">Treasury Transactions</h5>
 | 
			
		||||
                  <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>
 | 
			
		||||
                </span>
 | 
			
		||||
                <app-address-transactions-widget [addressSummary$]="walletSummary$"></app-address-transactions-widget>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
@ -162,9 +162,6 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
    this.cacheBlocksSubscription?.unsubscribe();
 | 
			
		||||
    this.networkChangedSubscription?.unsubscribe();
 | 
			
		||||
    this.queryParamsSubscription?.unsubscribe();
 | 
			
		||||
    this.blockGraphs.forEach(graph => {
 | 
			
		||||
      graph.destroy();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  shiftTestBlocks(): void {
 | 
			
		||||
 | 
			
		||||
@ -19,10 +19,12 @@
 | 
			
		||||
    } @else if (!user) {
 | 
			
		||||
      <!-- User not logged in -->
 | 
			
		||||
      <div class="alert alert-mempool d-block text-center w-100">
 | 
			
		||||
        <div class="d-inline align-middle pr-2">
 | 
			
		||||
          <span>To use the faucet, please</span>
 | 
			
		||||
        <div class="d-inline align-middle">
 | 
			
		||||
          <span>To use the faucet, please </span>
 | 
			
		||||
          <a routerLink="/login" [queryParams]="{'redirectTo': '/testnet4/faucet'}">login</a>
 | 
			
		||||
          <span class="mr-2"> or</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <app-github-login customClass="btn btn-sm" width="150px" redirectTo="/testnet4/faucet" buttonString="Sign up with"></app-github-login>
 | 
			
		||||
        <app-twitter-login customClass="btn btn-sm" width="220px" redirectTo="/testnet4/faucet" buttonString="Sign up with Twitter"></app-twitter-login>
 | 
			
		||||
      </div>
 | 
			
		||||
    }
 | 
			
		||||
    @else if (user && user.status === 'pending' && !user.email && user.snsId) {
 | 
			
		||||
@ -34,18 +36,18 @@
 | 
			
		||||
      </div>
 | 
			
		||||
    }
 | 
			
		||||
    @else if (error === 'not_available') {
 | 
			
		||||
      <!-- User logged in but not a paid user or did not link its Github account -->
 | 
			
		||||
      <!-- User logged in but not a paid user or did not link its Twitter account -->
 | 
			
		||||
      <div class="alert alert-mempool d-block text-center w-100">
 | 
			
		||||
        <div class="d-inline align-middle">
 | 
			
		||||
          <span class="mb-2 mr-2">To use the faucet, please</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <app-github-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your"></app-github-login>
 | 
			
		||||
        <app-twitter-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your Twitter"></app-twitter-login>
 | 
			
		||||
      </div>
 | 
			
		||||
    }
 | 
			
		||||
    @else if (error === 'account_limited') {
 | 
			
		||||
      <div class="alert alert-mempool d-block text-center w-100">
 | 
			
		||||
        <div class="d-inline align-middle">
 | 
			
		||||
          <span class="mb-2 mr-2">Your account does not allow you to access the faucet</span>
 | 
			
		||||
          <span class="mb-2 mr-2">Your Twitter account does not allow you to access the faucet</span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,7 @@ export class FaucetComponent implements OnInit, OnDestroy {
 | 
			
		||||
    min: number; // minimum amount to request at once (in sats)
 | 
			
		||||
    max: number; // maximum amount to request at once
 | 
			
		||||
    address?: string; // faucet address
 | 
			
		||||
    code: 'ok' | 'faucet_not_available' | 'faucet_maximum_reached' | 'faucet_too_soon' | 'faucet_not_available_no_utxo';
 | 
			
		||||
    code: 'ok' | 'faucet_not_available' | 'faucet_maximum_reached' | 'faucet_too_soon';
 | 
			
		||||
  } | null = null;
 | 
			
		||||
  faucetForm: FormGroup;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,6 +0,0 @@
 | 
			
		||||
<a href="#" (click)="githubLogin()" [class]="(disabled ? 'disabled': '') + (customClass ? customClass : 'w-100 btn mt-1 d-flex justify-content-center align-items-center')" style="background-color: rgb(31, 35, 40)" [style]="width ? 'width: ' + width : ''">
 | 
			
		||||
  <span class="ml-2 text-light align-middle">{{ buttonString }}</span>
 | 
			
		||||
  <svg height="32" viewBox="0 0 18 16" width="32" style="fill: white; padding-left: 5px">
 | 
			
		||||
    <path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
 | 
			
		||||
  </svg>
 | 
			
		||||
</a>
 | 
			
		||||
@ -1,25 +0,0 @@
 | 
			
		||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-github-login',
 | 
			
		||||
  templateUrl: './github-login.component.html',
 | 
			
		||||
})
 | 
			
		||||
export class GithubLogin {
 | 
			
		||||
  @Input() width: string | null = null;
 | 
			
		||||
  @Input() customClass: string | null = null;
 | 
			
		||||
  @Input() buttonString: string= 'unset';
 | 
			
		||||
  @Input() redirectTo: string | null = null;
 | 
			
		||||
  @Output() clicked = new EventEmitter<boolean>();
 | 
			
		||||
  @Input() disabled: boolean = false;
 | 
			
		||||
 | 
			
		||||
  constructor() {}
 | 
			
		||||
 | 
			
		||||
  githubLogin() {
 | 
			
		||||
    this.clicked.emit(true);
 | 
			
		||||
    if (this.redirectTo) {
 | 
			
		||||
      location.replace(`/api/v1/services/auth/login/github?redirectTo=${encodeURIComponent(this.redirectTo)}`);
 | 
			
		||||
    } else {
 | 
			
		||||
      location.replace(`/api/v1/services/auth/login/github?redirectTo=${location.href}`);
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -56,7 +56,8 @@
 | 
			
		||||
              </ng-template>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="timestamp text-left">
 | 
			
		||||
              <app-timestamp [customFormat]="'yyyy-MM-dd HH:mm'" [unixTime]="utxo.blocktime"></app-timestamp>
 | 
			
		||||
              ‎{{ utxo.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }}
 | 
			
		||||
              <div class="symbol lg-inline relative-time"><i>(<app-time kind="since" [time]="utxo.blocktime"></app-time>)</i></div>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="expires-in text-left" [ngStyle]="{ 'color': getGradientColor(utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate) }">
 | 
			
		||||
              {{ utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate < 0 ? -(utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate) : utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate }} <span i18n="shared.blocks" class="symbol">blocks</span>
 | 
			
		||||
 | 
			
		||||
@ -53,7 +53,8 @@
 | 
			
		||||
              </ng-container>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="timestamp text-left">
 | 
			
		||||
              <app-timestamp [customFormat]="'yyyy-MM-dd HH:mm'" [unixTime]="peg.blocktime"></app-timestamp>
 | 
			
		||||
              ‎{{ peg.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }}
 | 
			
		||||
              <div class="symbol lg-inline relative-time"><i>(<app-time kind="since" [time]="peg.blocktime"></app-time>)</i></div>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="amount text-right" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0, 'glow-effect': peg.amount < 0 && peg.bitcoinaddress && !peg.bitcointxid}">
 | 
			
		||||
              <app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true" [addPlus]="true"></app-amount>
 | 
			
		||||
 | 
			
		||||
@ -4,8 +4,9 @@
 | 
			
		||||
  <nav class="navbar navbar-expand-md navbar-dark">
 | 
			
		||||
  <!-- Hamburger -->
 | 
			
		||||
  <ng-container *ngIf="servicesEnabled">
 | 
			
		||||
    <div *ngIf="user" class="profile_image_container" (click)="hamburgerClick($event)">
 | 
			
		||||
      <img [src]="'/api/v1/services/account/images/' + user.username" class="profile_image" onError="this.src = '/resources/anon.svg'; this.className = 'anon'" />
 | 
			
		||||
    <div *ngIf="user" class="profile_image_container" [class]="{'anon': !user.imageMd5}" (click)="hamburgerClick($event)">
 | 
			
		||||
      <img *ngIf="user.imageMd5" [src]="'/api/v1/services/account/images/' + user.username + '/md5=' + user.imageMd5" class="profile_image">
 | 
			
		||||
      <app-svg-images style="color: lightgrey; fill: lightgray" *ngIf="!user.imageMd5" name="anon"></app-svg-images>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div *ngIf="false && user === null" class="profile_image_container" (click)="hamburgerClick($event)">
 | 
			
		||||
      <app-svg-images name="hamburger" height="40"></app-svg-images>
 | 
			
		||||
@ -22,7 +23,7 @@
 | 
			
		||||
      } @else {
 | 
			
		||||
        <ng-template [ngIf]="subdomain && enterpriseInfo">
 | 
			
		||||
          <div class="subdomain_container">
 | 
			
		||||
            <img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
 | 
			
		||||
            <img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="vertical-line"></div>
 | 
			
		||||
        </ng-template>
 | 
			
		||||
@ -42,7 +43,7 @@
 | 
			
		||||
    } @else {
 | 
			
		||||
      <ng-template [ngIf]="subdomain && enterpriseInfo">
 | 
			
		||||
        <div class="subdomain_container">
 | 
			
		||||
          <img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
 | 
			
		||||
          <img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="vertical-line"></div>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
 | 
			
		||||
@ -269,7 +269,7 @@ nav {
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  align-self: center;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  .anon {
 | 
			
		||||
  &.anon {
 | 
			
		||||
    border: 1.5px solid lightgrey;
 | 
			
		||||
    color: lightgrey;
 | 
			
		||||
    border-radius: 5px;
 | 
			
		||||
 | 
			
		||||
@ -120,7 +120,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    this.blockGraph?.destroy();
 | 
			
		||||
    this.blockSub.unsubscribe();
 | 
			
		||||
    this.timeLtrSubscription.unsubscribe();
 | 
			
		||||
    this.websocketService.stopTrackMempoolBlock();
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
      <h1 class="m-0 pt-1 pt-md-0">{{ poolStats.pool.name }}</h1>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="box pool-details">
 | 
			
		||||
    <div class="box">
 | 
			
		||||
      <div class="row">
 | 
			
		||||
 | 
			
		||||
        <div class="col-lg-6">
 | 
			
		||||
@ -173,125 +173,7 @@
 | 
			
		||||
    <div class="spinner-border text-light"></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) : ''">
 | 
			
		||||
                            @if ($index === 0 && branch) {
 | 
			
		||||
                              <a [routerLink]="['/tx' | relativeUrl, reverseHash(branch)]" class="cell-link">
 | 
			
		||||
                                <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 14px; color: white"></fa-icon>
 | 
			
		||||
                              </a>
 | 
			
		||||
                            }
 | 
			
		||||
                          </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 -->
 | 
			
		||||
  <h2 i18n="master-page.blocks">Blocks</h2>
 | 
			
		||||
  <table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5"
 | 
			
		||||
    [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()">
 | 
			
		||||
    <ng-container *ngIf="blocks$ | async as blocks; else skeleton">
 | 
			
		||||
@ -312,7 +194,7 @@
 | 
			
		||||
            <a [routerLink]="['/block' | relativeUrl, block.id]">{{ block.height }}</a>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="timestamp">
 | 
			
		||||
            <app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="block.timestamp" [hideTimeSince]="true"></app-timestamp>
 | 
			
		||||
            ‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="mined">
 | 
			
		||||
            <app-time kind="since" [time]="block.timestamp" [fastRender]="true" [showTooltip]="true"></app-time>
 | 
			
		||||
 | 
			
		||||
@ -49,110 +49,111 @@ div.scrollable {
 | 
			
		||||
  max-height: 75px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pool-details {
 | 
			
		||||
.box {
 | 
			
		||||
  padding-bottom: 5px;
 | 
			
		||||
  @media (min-width: 767.98px) {
 | 
			
		||||
    min-height: 187px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  .label {
 | 
			
		||||
    width: 25%;
 | 
			
		||||
    @media (min-width: 767.98px) {
 | 
			
		||||
      vertical-align: middle;
 | 
			
		||||
    }
 | 
			
		||||
    @media (max-width: 767.98px) {
 | 
			
		||||
      font-weight: bold;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .label.addresses {
 | 
			
		||||
    vertical-align: top;
 | 
			
		||||
    padding-top: 25px;
 | 
			
		||||
  }
 | 
			
		||||
  .addresses-data {
 | 
			
		||||
    vertical-align: top;
 | 
			
		||||
    font-family: monospace;
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .data {
 | 
			
		||||
    text-align: right;
 | 
			
		||||
    padding-left: 5%;
 | 
			
		||||
    @media (max-width: 992px) {
 | 
			
		||||
      text-align: left;
 | 
			
		||||
      padding-left: 12px;
 | 
			
		||||
    }
 | 
			
		||||
    @media (max-width: 450px) {
 | 
			
		||||
      text-align: right;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .progress {
 | 
			
		||||
    background-color: var(--secondary);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .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%;
 | 
			
		||||
    }
 | 
			
		||||
    @media (max-width: 650px) {
 | 
			
		||||
      width: 20%;
 | 
			
		||||
    }
 | 
			
		||||
    @media (max-width: 450px) {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .scriptmessage {
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    text-overflow: ellipsis;
 | 
			
		||||
.label {
 | 
			
		||||
  width: 25%;
 | 
			
		||||
  @media (min-width: 767.98px) {
 | 
			
		||||
    vertical-align: middle;
 | 
			
		||||
    width: auto;
 | 
			
		||||
    text-align: left;
 | 
			
		||||
  }
 | 
			
		||||
  @media (max-width: 767.98px) {
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.label.addresses {
 | 
			
		||||
  vertical-align: top;
 | 
			
		||||
  padding-top: 25px;
 | 
			
		||||
}
 | 
			
		||||
.addresses-data {
 | 
			
		||||
  vertical-align: top;
 | 
			
		||||
  font-family: monospace;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.data {
 | 
			
		||||
  text-align: right;
 | 
			
		||||
  padding-left: 5%;
 | 
			
		||||
  @media (max-width: 992px) {
 | 
			
		||||
    text-align: left;
 | 
			
		||||
    padding-left: 12px;
 | 
			
		||||
  }
 | 
			
		||||
  @media (max-width: 450px) {
 | 
			
		||||
    text-align: right;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.progress {
 | 
			
		||||
  background-color: var(--secondary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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%;
 | 
			
		||||
  }
 | 
			
		||||
  @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 {
 | 
			
		||||
@ -213,69 +214,4 @@ div.scrollable {
 | 
			
		||||
 | 
			
		||||
.taller-row {
 | 
			
		||||
  height: 75px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.stratum-table {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
 | 
			
		||||
  .merkle {
 | 
			
		||||
    width: 100px;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .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;
 | 
			
		||||
 | 
			
		||||
    .cell-link {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 0;
 | 
			
		||||
      left: 0;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      color: inherit;
 | 
			
		||||
      text-decoration: none;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      justify-content: center;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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,9 +10,6 @@ import { selectPowerOfTen } from '@app/bitcoin.utils';
 | 
			
		||||
import { formatNumber } from '@angular/common';
 | 
			
		||||
import { SeoService } from '@app/services/seo.service';
 | 
			
		||||
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 {
 | 
			
		||||
  cost: number,
 | 
			
		||||
@ -30,16 +27,12 @@ export class PoolComponent implements OnInit {
 | 
			
		||||
  @Input() left: number | string = 75;
 | 
			
		||||
 | 
			
		||||
  gfg = true;
 | 
			
		||||
  stratumEnabled = this.stateService.env.STRATUM_ENABLED;
 | 
			
		||||
 | 
			
		||||
  formatNumber = formatNumber;
 | 
			
		||||
  Math = Math;
 | 
			
		||||
  slugSubscription: Subscription;
 | 
			
		||||
  poolStats$: Observable<PoolStat>;
 | 
			
		||||
  blocks$: Observable<BlockExtended[]>;
 | 
			
		||||
  oobFees$: Observable<AccelerationTotal[]>;
 | 
			
		||||
  job$: Observable<StratumJob | null>;
 | 
			
		||||
  expectedBlockTime$: Observable<number>;
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
  error: HttpErrorResponse | null = null;
 | 
			
		||||
 | 
			
		||||
@ -60,8 +53,6 @@ export class PoolComponent implements OnInit {
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private websocketService: WebsocketService,
 | 
			
		||||
    private miningService: MiningService,
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.auditAvailable = this.stateService.env.AUDIT;
 | 
			
		||||
@ -71,7 +62,7 @@ export class PoolComponent implements OnInit {
 | 
			
		||||
    this.slugSubscription = this.route.params.pipe(map((params) => params.slug)).subscribe((slug) => {
 | 
			
		||||
      this.isLoading = true;
 | 
			
		||||
      this.blocks = [];
 | 
			
		||||
      this.chartOptions = {};
 | 
			
		||||
      this.chartOptions = {};  
 | 
			
		||||
      this.slug = slug;
 | 
			
		||||
      this.initializeObservables();
 | 
			
		||||
    });
 | 
			
		||||
@ -138,31 +129,6 @@ export class PoolComponent implements OnInit {
 | 
			
		||||
      }),
 | 
			
		||||
      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) {
 | 
			
		||||
@ -361,10 +327,6 @@ export class PoolComponent implements OnInit {
 | 
			
		||||
    return block.height;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  reverseHash(hash: string) {
 | 
			
		||||
    return hash.match(/../g).reverse().join('');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    this.slugSubscription.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -1,34 +0,0 @@
 | 
			
		||||
.accept-results {
 | 
			
		||||
  td, th {
 | 
			
		||||
    &.allowed {
 | 
			
		||||
      width: 10%;
 | 
			
		||||
      text-align: center;
 | 
			
		||||
    }
 | 
			
		||||
    &.txid {
 | 
			
		||||
      width: 50%;
 | 
			
		||||
    }
 | 
			
		||||
    &.rate {
 | 
			
		||||
      width: 20%;
 | 
			
		||||
      text-align: right;
 | 
			
		||||
      white-space: wrap;
 | 
			
		||||
    }
 | 
			
		||||
    &.reason {
 | 
			
		||||
      width: 20%;
 | 
			
		||||
      text-align: right;
 | 
			
		||||
      white-space: wrap;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @media (max-width: 950px) {
 | 
			
		||||
    table-layout: auto;
 | 
			
		||||
 | 
			
		||||
    td, th {
 | 
			
		||||
      &.allowed {
 | 
			
		||||
        width: 100px;
 | 
			
		||||
      }
 | 
			
		||||
      &.txid {
 | 
			
		||||
        max-width: 200px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -19,9 +19,6 @@
 | 
			
		||||
            <th class="rtt only-small">RTT</th>
 | 
			
		||||
            <th class="rtt only-large">RTT</th>
 | 
			
		||||
            <th class="height">Height</th>
 | 
			
		||||
            <th class="frontend only-large">Front</th>
 | 
			
		||||
            <th class="backend only-large">Back</th>
 | 
			
		||||
            <th class="electrs only-large">Electrs</th>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr *ngFor="let host of hosts; let i = index; trackBy: trackByFn">
 | 
			
		||||
            <td class="rank">{{ i + 1 }}</td>
 | 
			
		||||
@ -31,15 +28,6 @@
 | 
			
		||||
            <td class="rtt only-small">{{ (host.rtt / 1000) | number : '1.1-1' }} {{ host.rtt == null ? '' : 's'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
 | 
			
		||||
            <td class="rtt only-large">{{ host.rtt | number : '1.0-0' }} {{ host.rtt == null ? '' : 'ms'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
 | 
			
		||||
            <td class="height">{{ host.latestHeight }} {{ !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '✅')) }}</td>
 | 
			
		||||
            <ng-container *ngFor="let type of ['frontend', 'backend', 'electrs']">
 | 
			
		||||
              <td class="{{type}} only-large" [style.background-color]="host.hashes?.[type] ? '#' + host.hashes[type].slice(0, 6) : ''">
 | 
			
		||||
                @if (host.hashes?.[type]) {
 | 
			
		||||
                  <a [style.color]="'white'" href="https://github.com/mempool/{{type === 'electrs' ? 'electrs' : 'mempool'}}/commit/{{ host.hashes[type] }}" target="_blank">{{ host.hashes[type].slice(0, 8) || '?' }}</a>
 | 
			
		||||
                } @else {
 | 
			
		||||
                  <span>?</span>
 | 
			
		||||
                }
 | 
			
		||||
              </td>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </tbody>
 | 
			
		||||
      </table>
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .status-panel {
 | 
			
		||||
    max-width: 1000px;
 | 
			
		||||
    max-width: 720px;
 | 
			
		||||
    margin: auto;
 | 
			
		||||
    padding: 1em;
 | 
			
		||||
    background: var(--box-bg);
 | 
			
		||||
 | 
			
		||||
@ -82,10 +82,6 @@ export class ServerHealthComponent implements OnInit {
 | 
			
		||||
      return '🇺🇸';
 | 
			
		||||
    } else if (host.includes('.va1.')) {
 | 
			
		||||
      return '🇺🇸';
 | 
			
		||||
    } else if (host.includes('.sg1.')) {
 | 
			
		||||
      return '🇸🇬';
 | 
			
		||||
    } else if (host.includes('.hnl.')) {
 | 
			
		||||
      return '🤙';
 | 
			
		||||
    } else {
 | 
			
		||||
      return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,55 +0,0 @@
 | 
			
		||||
<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="merkle" [attr.colspan]="rows[0]?.merkleCells?.length || 4">
 | 
			
		||||
            Merkle Branches
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="pool">Pool</td>
 | 
			
		||||
          <td class="tag">Coinbase Tag</td>
 | 
			
		||||
          <td class="reward">Reward</td>
 | 
			
		||||
          <td class="height">Height</td>
 | 
			
		||||
        </tr>
 | 
			
		||||
      </thead>
 | 
			
		||||
      <tbody>
 | 
			
		||||
        @for (row of rows; track row.job.pool) {
 | 
			
		||||
          <tr>
 | 
			
		||||
            @for (cell of row.merkleCells; track $index) {
 | 
			
		||||
              <td class="merkle" [style.background-color]="cell.hash ? '#' + cell.hash.slice(0, 6) : ''">
 | 
			
		||||
                @if ($index === 0 && cell.hash) {
 | 
			
		||||
                  <a [routerLink]="['/tx' | relativeUrl, reverseHash(cell.hash)]" class="cell-link">
 | 
			
		||||
                    <div class="pipe-segment" [class]="pipeToClass(cell.type)"></div>
 | 
			
		||||
                  </a>
 | 
			
		||||
                } @else {
 | 
			
		||||
                  <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>
 | 
			
		||||
            <td class="tag">
 | 
			
		||||
              {{ row.job.tag }}
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="reward">
 | 
			
		||||
              <app-amount [satoshis]="row.job.reward"></app-amount>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="height">
 | 
			
		||||
              {{ row.job.height }}
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        }
 | 
			
		||||
      </tbody>
 | 
			
		||||
    </table>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,138 +0,0 @@
 | 
			
		||||
.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;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .cell-link {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    color: inherit;
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 800px) {
 | 
			
		||||
  .stratum-table {
 | 
			
		||||
    td {
 | 
			
		||||
      &.tag {
 | 
			
		||||
        display: none;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 650px) {
 | 
			
		||||
  .stratum-table {
 | 
			
		||||
    td {
 | 
			
		||||
      &.reward {
 | 
			
		||||
        display: none;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.badge {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  color: #FFF;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pool-logo {
 | 
			
		||||
  width: 15px;
 | 
			
		||||
  height: 15px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  top: -1px;
 | 
			
		||||
  margin-right: 2px;
 | 
			
		||||
}
 | 
			
		||||
@ -1,230 +0,0 @@
 | 
			
		||||
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, poolId: number): string[] {
 | 
			
		||||
  let lastHash = '';
 | 
			
		||||
  const ids: string[] = [];
 | 
			
		||||
  for (let i = 0; i < numBranches; i++) {
 | 
			
		||||
    if (merkleBranches[i]) {
 | 
			
		||||
      lastHash = merkleBranches[i];
 | 
			
		||||
      ids.push(`${i}-${lastHash}`);
 | 
			
		||||
    } else {
 | 
			
		||||
      ids.push(`${i}-${lastHash}-${poolId}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  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, job.pool) };
 | 
			
		||||
    }
 | 
			
		||||
    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];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  reverseHash(hash: string) {
 | 
			
		||||
    return hash.match(/../g).reverse().join('');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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 '│';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,47 +1,4 @@
 | 
			
		||||
<ng-container [ngSwitch]="name">
 | 
			
		||||
  <ng-container *ngSwitchCase="'VISA'">
 | 
			
		||||
    <svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" viewBox="0 0 576 512" fill="white" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
      <!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
 | 
			
		||||
      <path d="M470.1 231.3s7.6 37.2 9.3 45H446c3.3-8.9 16-43.5 16-43.5-.2 .3 3.3-9.1 5.3-14.9l2.8 13.4zM576 80v352c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V80c0-26.5 21.5-48 48-48h480c26.5 0 48 21.5 48 48zM152.5 331.2L215.7 176h-42.5l-39.3 106-4.3-21.5-14-71.4c-2.3-9.9-9.4-12.7-18.2-13.1H32.7l-.7 3.1c15.8 4 29.9 9.8 42.2 17.1l35.8 135h42.5zm94.4 .2L272.1 176h-40.2l-25.1 155.4h40.1zm139.9-50.8c.2-17.7-10.6-31.2-33.7-42.3-14.1-7.1-22.7-11.9-22.7-19.2 .2-6.6 7.3-13.4 23.1-13.4 13.1-.3 22.7 2.8 29.9 5.9l3.6 1.7 5.5-33.6c-7.9-3.1-20.5-6.6-36-6.6-39.7 0-67.6 21.2-67.8 51.4-.3 22.3 20 34.7 35.2 42.2 15.5 7.6 20.8 12.6 20.8 19.3-.2 10.4-12.6 15.2-24.1 15.2-16 0-24.6-2.5-37.7-8.3l-5.3-2.5-5.6 34.9c9.4 4.3 26.8 8.1 44.8 8.3 42.2 .1 69.7-20.8 70-53zM528 331.4L495.6 176h-31.1c-9.6 0-16.9 2.8-21 12.9l-59.7 142.5H426s6.9-19.2 8.4-23.3H486c1.2 5.5 4.8 23.3 4.8 23.3H528z"/>
 | 
			
		||||
    </svg>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
 | 
			
		||||
  <ng-container *ngSwitchCase="'MASTERCARD'">
 | 
			
		||||
    <svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" viewBox="0 0 576 512" fill="white" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
      <!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M482.9 410.3c0 6.8-4.6 11.7-11.2 11.7-6.8 0-11.2-5.2-11.2-11.7 0-6.5 4.4-11.7 11.2-11.7 6.6 0 11.2 5.2 11.2 11.7zm-310.8-11.7c-7.1 0-11.2 5.2-11.2 11.7 0 6.5 4.1 11.7 11.2 11.7 6.5 0 10.9-4.9 10.9-11.7-.1-6.5-4.4-11.7-10.9-11.7zm117.5-.3c-5.4 0-8.7 3.5-9.5 8.7h19.1c-.9-5.7-4.4-8.7-9.6-8.7zm107.8 .3c-6.8 0-10.9 5.2-10.9 11.7 0 6.5 4.1 11.7 10.9 11.7 6.8 0 11.2-4.9 11.2-11.7 0-6.5-4.4-11.7-11.2-11.7zm105.9 26.1c0 .3 .3 .5 .3 1.1 0 .3-.3 .5-.3 1.1-.3 .3-.3 .5-.5 .8-.3 .3-.5 .5-1.1 .5-.3 .3-.5 .3-1.1 .3-.3 0-.5 0-1.1-.3-.3 0-.5-.3-.8-.5-.3-.3-.5-.5-.5-.8-.3-.5-.3-.8-.3-1.1 0-.5 0-.8 .3-1.1 0-.5 .3-.8 .5-1.1 .3-.3 .5-.3 .8-.5 .5-.3 .8-.3 1.1-.3 .5 0 .8 0 1.1 .3 .5 .3 .8 .3 1.1 .5s.2 .6 .5 1.1zm-2.2 1.4c.5 0 .5-.3 .8-.3 .3-.3 .3-.5 .3-.8 0-.3 0-.5-.3-.8-.3 0-.5-.3-1.1-.3h-1.6v3.5h.8V426h.3l1.1 1.4h.8l-1.1-1.3zM576 81v352c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V81c0-26.5 21.5-48 48-48h480c26.5 0 48 21.5 48 48zM64 220.6c0 76.5 62.1 138.5 138.5 138.5 27.2 0 53.9-8.2 76.5-23.1-72.9-59.3-72.4-171.2 0-230.5-22.6-15-49.3-23.1-76.5-23.1-76.4-.1-138.5 62-138.5 138.2zm224 108.8c70.5-55 70.2-162.2 0-217.5-70.2 55.3-70.5 162.6 0 217.5zm-142.3 76.3c0-8.7-5.7-14.4-14.7-14.7-4.6 0-9.5 1.4-12.8 6.5-2.4-4.1-6.5-6.5-12.2-6.5-3.8 0-7.6 1.4-10.6 5.4V392h-8.2v36.7h8.2c0-18.9-2.5-30.2 9-30.2 10.2 0 8.2 10.2 8.2 30.2h7.9c0-18.3-2.5-30.2 9-30.2 10.2 0 8.2 10 8.2 30.2h8.2v-23zm44.9-13.7h-7.9v4.4c-2.7-3.3-6.5-5.4-11.7-5.4-10.3 0-18.2 8.2-18.2 19.3 0 11.2 7.9 19.3 18.2 19.3 5.2 0 9-1.9 11.7-5.4v4.6h7.9V392zm40.5 25.6c0-15-22.9-8.2-22.9-15.2 0-5.7 11.9-4.8 18.5-1.1l3.3-6.5c-9.4-6.1-30.2-6-30.2 8.2 0 14.3 22.9 8.3 22.9 15 0 6.3-13.5 5.8-20.7 .8l-3.5 6.3c11.2 7.6 32.6 6 32.6-7.5zm35.4 9.3l-2.2-6.8c-3.8 2.1-12.2 4.4-12.2-4.1v-16.6h13.1V392h-13.1v-11.2h-8.2V392h-7.6v7.3h7.6V416c0 17.6 17.3 14.4 22.6 10.9zm13.3-13.4h27.5c0-16.2-7.4-22.6-17.4-22.6-10.6 0-18.2 7.9-18.2 19.3 0 20.5 22.6 23.9 33.8 14.2l-3.8-6c-7.8 6.4-19.6 5.8-21.9-4.9zm59.1-21.5c-4.6-2-11.6-1.8-15.2 4.4V392h-8.2v36.7h8.2V408c0-11.6 9.5-10.1 12.8-8.4l2.4-7.6zm10.6 18.3c0-11.4 11.6-15.1 20.7-8.4l3.8-6.5c-11.6-9.1-32.7-4.1-32.7 15 0 19.8 22.4 23.8 32.7 15l-3.8-6.5c-9.2 6.5-20.7 2.6-20.7-8.6zm66.7-18.3H408v4.4c-8.3-11-29.9-4.8-29.9 13.9 0 19.2 22.4 24.7 29.9 13.9v4.6h8.2V392zm33.7 0c-2.4-1.2-11-2.9-15.2 4.4V392h-7.9v36.7h7.9V408c0-11 9-10.3 12.8-8.4l2.4-7.6zm40.3-14.9h-7.9v19.3c-8.2-10.9-29.9-5.1-29.9 13.9 0 19.4 22.5 24.6 29.9 13.9v4.6h7.9v-51.7zm7.6-75.1v4.6h.8V302h1.9v-.8h-4.6v.8h1.9zm6.6 123.8c0-.5 0-1.1-.3-1.6-.3-.3-.5-.8-.8-1.1-.3-.3-.8-.5-1.1-.8-.5 0-1.1-.3-1.6-.3-.3 0-.8 .3-1.4 .3-.5 .3-.8 .5-1.1 .8-.5 .3-.8 .8-.8 1.1-.3 .5-.3 1.1-.3 1.6 0 .3 0 .8 .3 1.4 0 .3 .3 .8 .8 1.1 .3 .3 .5 .5 1.1 .8 .5 .3 1.1 .3 1.4 .3 .5 0 1.1 0 1.6-.3 .3-.3 .8-.5 1.1-.8 .3-.3 .5-.8 .8-1.1 .3-.6 .3-1.1 .3-1.4zm3.2-124.7h-1.4l-1.6 3.5-1.6-3.5h-1.4v5.4h.8v-4.1l1.6 3.5h1.1l1.4-3.5v4.1h1.1v-5.4zm4.4-80.5c0-76.2-62.1-138.3-138.5-138.3-27.2 0-53.9 8.2-76.5 23.1 72.1 59.3 73.2 171.5 0 230.5 22.6 15 49.5 23.1 76.5 23.1 76.4 .1 138.5-61.9 138.5-138.4z"/>
 | 
			
		||||
    </svg>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
 | 
			
		||||
  <ng-container *ngSwitchCase="'JCB'">
 | 
			
		||||
    <svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" viewBox="0 0 576 512" fill="white" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
      <!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M431.5 244.3V212c41.2 0 38.5 .2 38.5 .2 7.3 1.3 13.3 7.3 13.3 16 0 8.8-6 14.5-13.3 15.8-1.2 .4-3.3 .3-38.5 .3zm42.8 20.2c-2.8-.7-3.3-.5-42.8-.5v35c39.6 0 40 .2 42.8-.5 7.5-1.5 13.5-8 13.5-17 0-8.7-6-15.5-13.5-17zM576 80v352c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V80c0-26.5 21.5-48 48-48h480c26.5 0 48 21.5 48 48zM182 192.3h-57c0 67.1 10.7 109.7-35.8 109.7-19.5 0-38.8-5.7-57.2-14.8v28c30 8.3 68 8.3 68 8.3 97.9 0 82-47.7 82-131.2zm178.5 4.5c-63.4-16-165-14.9-165 59.3 0 77.1 108.2 73.6 165 59.2V287C312.9 311.7 253 309 253 256s59.8-55.6 107.5-31.2v-28zM544 286.5c0-18.5-16.5-30.5-38-32v-.8c19.5-2.7 30.3-15.5 30.3-30.2 0-19-15.7-30-37-31 0 0 6.3-.3-120.3-.3v127.5h122.7c24.3 .1 42.3-12.9 42.3-33.2z"/>
 | 
			
		||||
    </svg>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
 | 
			
		||||
  <ng-container *ngSwitchCase="'DISCOVER'">
 | 
			
		||||
    <svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" viewBox="0 0 576 512" fill="white" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
      <!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M520.4 196.1c0-7.9-5.5-12.1-15.6-12.1h-4.9v24.9h4.7c10.3 0 15.8-4.4 15.8-12.8zM528 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h480c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-44.1 138.9c22.6 0 52.9-4.1 52.9 24.4 0 12.6-6.6 20.7-18.7 23.2l25.8 34.4h-19.6l-22.2-32.8h-2.2v32.8h-16zm-55.9 .1h45.3v14H444v18.2h28.3V217H444v22.2h29.3V253H428zm-68.7 0l21.9 55.2 22.2-55.2h17.5l-35.5 84.2h-8.6l-35-84.2zm-55.9-3c24.7 0 44.6 20 44.6 44.6 0 24.7-20 44.6-44.6 44.6-24.7 0-44.6-20-44.6-44.6 0-24.7 20-44.6 44.6-44.6zm-49.3 6.1v19c-20.1-20.1-46.8-4.7-46.8 19 0 25 27.5 38.5 46.8 19.2v19c-29.7 14.3-63.3-5.7-63.3-38.2 0-31.2 33.1-53 63.3-38zm-97.2 66.3c11.4 0 22.4-15.3-3.3-24.4-15-5.5-20.2-11.4-20.2-22.7 0-23.2 30.6-31.4 49.7-14.3l-8.4 10.8c-10.4-11.6-24.9-6.2-24.9 2.5 0 4.4 2.7 6.9 12.3 10.3 18.2 6.6 23.6 12.5 23.6 25.6 0 29.5-38.8 37.4-56.6 11.3l10.3-9.9c3.7 7.1 9.9 10.8 17.5 10.8zM55.4 253H32v-82h23.4c26.1 0 44.1 17 44.1 41.1 0 18.5-13.2 40.9-44.1 40.9zm67.5 0h-16v-82h16zM544 433c0 8.2-6.8 15-15 15H128c189.6-35.6 382.7-139.2 416-160zM74.1 191.6c-5.2-4.9-11.6-6.6-21.9-6.6H48v54.2h4.2c10.3 0 17-2 21.9-6.4 5.7-5.2 8.9-12.8 8.9-20.7s-3.2-15.5-8.9-20.5z"/>
 | 
			
		||||
    </svg>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
 | 
			
		||||
  <ng-container *ngSwitchCase="'DISCOVER_DINERS'">
 | 
			
		||||
    <svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" viewBox="0 0 576 512" fill="white" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
      <!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M239.7 79.9c-96.9 0-175.8 78.6-175.8 175.8 0 96.9 78.9 175.8 175.8 175.8 97.2 0 175.8-78.9 175.8-175.8 0-97.2-78.6-175.8-175.8-175.8zm-39.9 279.6c-41.7-15.9-71.4-56.4-71.4-103.8s29.7-87.9 71.4-104.1v207.9zm79.8 .3V151.6c41.7 16.2 71.4 56.7 71.4 104.1s-29.7 87.9-71.4 104.1zM528 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h480c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zM329.7 448h-90.3c-106.2 0-193.8-85.5-193.8-190.2C45.6 143.2 133.2 64 239.4 64h90.3c105 0 200.7 79.2 200.7 193.8 0 104.7-95.7 190.2-200.7 190.2z"/>
 | 
			
		||||
    </svg>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
 | 
			
		||||
  <ng-container *ngSwitchCase="'AMERICAN_EXPRESS'">
 | 
			
		||||
    <svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" viewBox="0 0 576 512" fill="white" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
      <!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M0 432c0 26.5 21.5 48 48 48H528c26.5 0 48-21.5 48-48v-1.1H514.3l-31.9-35.1-31.9 35.1H246.8V267.1H181L262.7 82.4h78.6l28.1 63.2V82.4h97.2L483.5 130l17-47.6H576V80c0-26.5-21.5-48-48-48H48C21.5 32 0 53.5 0 80V432zm440.4-21.7L482.6 364l42 46.3H576l-68-72.1 68-72.1H525.4l-42 46.7-41.5-46.7H390.5L458 338.6l-67.4 71.6V377.1h-83V354.9h80.9V322.6H307.6V300.2h83V267.1h-122V410.3H440.4zm96.3-72L576 380.2V296.9l-39.3 41.4zm-36.3-92l36.9-100.6V246.3H576V103H515.8l-32.2 89.3L451.7 103H390.5V246.1L327.3 103H276.1L213.7 246.3h43l11.9-28.7h65.9l12 28.7h82.7V146L466 246.3h34.4zM282 185.4l19.5-46.9 19.4 46.9H282z"/>
 | 
			
		||||
    </svg>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
 | 
			
		||||
  <ng-container *ngSwitchCase="'OTHER_BRAND'">
 | 
			
		||||
    <svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" viewBox="0 0 576 512" fill="white" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
      <!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M512 80c8.8 0 16 7.2 16 16l0 32L48 128l0-32c0-8.8 7.2-16 16-16l448 0zm16 144l0 192c0 8.8-7.2 16-16 16L64 432c-8.8 0-16-7.2-16-16l0-192 480 0zM64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l448 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32zm56 304c-13.3 0-24 10.7-24 24s10.7 24 24 24l48 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-48 0zm128 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l112 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-112 0z"/>
 | 
			
		||||
    </svg>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
 | 
			
		||||
  <ng-container *ngSwitchCase="'officialMempoolSpace'">
 | 
			
		||||
    <svg [class]="class" [style]="style" [attr.width]="width" [attr.height]="height" [attr.viewBox]="viewBox" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
      <path d="M163.658 113.263C161.089 113.263 158.992 111.146 158.992 108.535C158.992 105.966 161.048 103.951 163.658 103.951C166.269 103.951 168.325 105.966 168.325 108.535C168.325 111.125 166.228 113.263 163.658 113.263Z" fill="#9857FF"/>
 | 
			
		||||
 | 
			
		||||
@ -1,8 +0,0 @@
 | 
			
		||||
<div [formGroup]="timezoneForm" class="text-small text-center">
 | 
			
		||||
    <select formControlName="mode" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 110px;" (change)="changeMode()">
 | 
			
		||||
        <option value="local">UTC{{ localTimezoneOffset !== '+0' ? localTimezoneOffset : '' }} {{ localTimezoneName ? '- ' + localTimezoneName : '' }}</option>
 | 
			
		||||
        <option value="+0" *ngIf="localTimezoneOffset !== '+0'">UTC - Greenwich Mean Time (GMT)</option>
 | 
			
		||||
        <option disabled>────</option>
 | 
			
		||||
        <option *ngFor="let timezone of timezones" [value]="timezone.offset">UTC{{ timezone.offset !== '+0' ? timezone.offset : '' }} - {{ timezone.name }}</option>
 | 
			
		||||
    </select>
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,58 +0,0 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 | 
			
		||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
 | 
			
		||||
import { StorageService } from '@app/services/storage.service';
 | 
			
		||||
import { StateService } from '@app/services/state.service';
 | 
			
		||||
import { timezones } from '@app/app.constants';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-timezone-selector',
 | 
			
		||||
  templateUrl: './timezone-selector.component.html',
 | 
			
		||||
  styleUrls: ['./timezone-selector.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush
 | 
			
		||||
})
 | 
			
		||||
export class TimezoneSelectorComponent implements OnInit {
 | 
			
		||||
  timezoneForm: UntypedFormGroup;
 | 
			
		||||
  timezones = timezones;
 | 
			
		||||
  localTimezoneOffset: string = '';
 | 
			
		||||
  localTimezoneName: string;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private formBuilder: UntypedFormBuilder,
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
    private storageService: StorageService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.setLocalTimezone();
 | 
			
		||||
    this.timezoneForm = this.formBuilder.group({
 | 
			
		||||
      mode: ['local'],
 | 
			
		||||
    });
 | 
			
		||||
    this.stateService.timezone$.subscribe((mode) => {
 | 
			
		||||
      this.timezoneForm.get('mode')?.setValue(mode);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  changeMode() {
 | 
			
		||||
    const newMode = this.timezoneForm.get('mode')?.value;
 | 
			
		||||
    this.storageService.setValue('timezone-preference', newMode);
 | 
			
		||||
    this.stateService.timezone$.next(newMode);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setLocalTimezone() {
 | 
			
		||||
    const offset = new Date().getTimezoneOffset();
 | 
			
		||||
    const sign = offset <= 0 ? "+" : "-";
 | 
			
		||||
    const absOffset = Math.abs(offset);
 | 
			
		||||
    const hours = String(Math.floor(absOffset / 60));
 | 
			
		||||
    const minutes = String(absOffset % 60).padStart(2, '0');
 | 
			
		||||
    if (minutes === '00') {
 | 
			
		||||
      this.localTimezoneOffset = `${sign}${hours}`;
 | 
			
		||||
    } else {
 | 
			
		||||
      this.localTimezoneOffset = `${sign}${hours.padStart(2, '0')}:${minutes}`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const timezone = this.timezones.find(tz => tz.offset === this.localTimezoneOffset);
 | 
			
		||||
    this.timezones = this.timezones.filter(tz => tz.offset !== this.localTimezoneOffset && tz.offset !== '+0');
 | 
			
		||||
    this.localTimezoneName = timezone ? timezone.name : '';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -8,7 +8,7 @@
 | 
			
		||||
          </a>
 | 
			
		||||
        } @else if (enterpriseInfo?.img || enterpriseInfo?.imageMd5) {
 | 
			
		||||
          <a [routerLink]="['/' | relativeUrl]">
 | 
			
		||||
            <img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + enterpriseInfo.name + '/logo'" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
 | 
			
		||||
            <img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + enterpriseInfo.name + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
 | 
			
		||||
          </a>
 | 
			
		||||
          <div class="vertical-line"></div>
 | 
			
		||||
        }
 | 
			
		||||
@ -88,7 +88,7 @@
 | 
			
		||||
          <div class="field narrower mt-2">
 | 
			
		||||
            <div class="label" i18n="transaction.confirmed-at">Confirmed at</div>
 | 
			
		||||
            <div class="value">
 | 
			
		||||
              <app-timestamp [customFormat]="'yyyy-MM-dd HH:mm'" [unixTime]="tx.status.block_time" [hideTimeSince]="true"></app-timestamp>
 | 
			
		||||
              ‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
 | 
			
		||||
              <div class="lg-inline">
 | 
			
		||||
                <i class="symbol">(<app-time kind="since" [time]="tx.status.block_time" [fastRender]="true" [showTooltip]="true"></app-time>)</i>
 | 
			
		||||
              </div>
 | 
			
		||||
@ -124,6 +124,7 @@
 | 
			
		||||
          <ng-container *ngIf="(ETA$ | async) as eta;">
 | 
			
		||||
            <app-accelerate-checkout
 | 
			
		||||
              *ngIf="(da$ | async) as da;"
 | 
			
		||||
              [cashappEnabled]="cashappEligible"
 | 
			
		||||
              [advancedEnabled]="false"
 | 
			
		||||
              [forceMobile]="true"
 | 
			
		||||
              [tx]="tx"
 | 
			
		||||
 | 
			
		||||
@ -756,6 +756,10 @@ export class TrackerComponent implements OnInit, OnDestroy {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get cashappEligible(): boolean {
 | 
			
		||||
    return this.mempoolPosition?.block > 0 && this.tx.weight < 4000;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get showAccelerationSummary(): boolean {
 | 
			
		||||
    return (
 | 
			
		||||
      this.tx
 | 
			
		||||
 | 
			
		||||
@ -130,6 +130,16 @@
 | 
			
		||||
            <p>The mempool Blocks 3 | 2 Logo</p>
 | 
			
		||||
            <br><br>
 | 
			
		||||
 | 
			
		||||
            <img src="/resources/memepool-logo.png" style="width: 500px; max-width: 80%">
 | 
			
		||||
            <br><br>
 | 
			
		||||
            <p>The memepool Logo</p>
 | 
			
		||||
            <br><br>
 | 
			
		||||
 | 
			
		||||
            <img src="/resources/mempoo-logo.png" style="width: 500px; max-width: 80%">
 | 
			
		||||
            <br><br>
 | 
			
		||||
            <p>The mempoo Logo</p>
 | 
			
		||||
            <br><br>
 | 
			
		||||
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <br>
 | 
			
		||||
 | 
			
		||||
@ -61,7 +61,10 @@
 | 
			
		||||
    <tr>
 | 
			
		||||
      <td i18n="block.timestamp">Timestamp</td>
 | 
			
		||||
      <td>
 | 
			
		||||
        <app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="tx.status.block_time"></app-timestamp>
 | 
			
		||||
        ‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
 | 
			
		||||
        <div class="lg-inline">
 | 
			
		||||
          <i class="symbol">(<app-time kind="since" [time]="tx.status.block_time" [fastRender]="true"></app-time>)</i>
 | 
			
		||||
        </div>
 | 
			
		||||
      </td>
 | 
			
		||||
    </tr>
 | 
			
		||||
  } @else {
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,6 @@
 | 
			
		||||
          [height]="tx?.status?.block_height"
 | 
			
		||||
          [replaced]="replaced"
 | 
			
		||||
          [removed]="this.rbfInfo?.mined && !this.tx?.status?.confirmed"
 | 
			
		||||
          [cached]="isCached"
 | 
			
		||||
        ></app-confirmations>
 | 
			
		||||
      </div>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
@ -139,6 +138,7 @@
 | 
			
		||||
 | 
			
		||||
      <app-accelerate-checkout
 | 
			
		||||
        *ngIf="(da$ | async) as da;"
 | 
			
		||||
        [cashappEnabled]="cashappEligible"
 | 
			
		||||
        [advancedEnabled]="true"
 | 
			
		||||
        [tx]="tx"
 | 
			
		||||
        [accelerating]="isAcceleration"
 | 
			
		||||
 | 
			
		||||
@ -156,6 +156,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
  showAccelerationDetails = false;
 | 
			
		||||
  hasAccelerationDetails = false;
 | 
			
		||||
  scrollIntoAccelPreview = false;
 | 
			
		||||
  cashappEligible = false;
 | 
			
		||||
  auditEnabled: boolean = this.stateService.env.AUDIT && this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true;
 | 
			
		||||
  isMempoolSpaceBuild = this.stateService.isMempoolSpaceBuild;
 | 
			
		||||
 | 
			
		||||
@ -239,7 +240,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
        retry({ count: 2, delay: 2000 }),
 | 
			
		||||
        // Try again until we either get a valid response, or the transaction is confirmed
 | 
			
		||||
        repeat({ delay: 2000 }),
 | 
			
		||||
        filter((transactionTimes) => transactionTimes?.[0] > 0 || this.tx.status?.confirmed),
 | 
			
		||||
        filter((transactionTimes) => transactionTimes?.length && transactionTimes[0] > 0 && !this.tx.status?.confirmed),
 | 
			
		||||
        take(1),
 | 
			
		||||
      )),
 | 
			
		||||
    )
 | 
			
		||||
@ -527,6 +528,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
                  this.miningStats = stats;
 | 
			
		||||
                });
 | 
			
		||||
              }
 | 
			
		||||
              if (txPosition.position?.block > 0 && this.tx.weight < 4000) {
 | 
			
		||||
                this.cashappEligible = true;
 | 
			
		||||
              }
 | 
			
		||||
              if (!this.gotInitialPosition && txPosition.position?.block === 0 && txPosition.position?.vsize < 750_000) {
 | 
			
		||||
                this.accelerationFlowCompleted = true;
 | 
			
		||||
              }
 | 
			
		||||
@ -1032,6 +1036,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
    this.showAccelerationDetails = false;
 | 
			
		||||
    this.accelerationFlowCompleted = false;
 | 
			
		||||
    this.accelerationInfo = null;
 | 
			
		||||
    this.cashappEligible = false;
 | 
			
		||||
    this.txInBlockIndex = null;
 | 
			
		||||
    this.mempoolPosition = null;
 | 
			
		||||
    this.pool = null;
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@
 | 
			
		||||
      <app-truncate [text]="tx.txid"></app-truncate>
 | 
			
		||||
    </a>
 | 
			
		||||
    <div>
 | 
			
		||||
      <ng-template [ngIf]="tx.status.confirmed"><app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="tx.status.block_time" [hideTimeSince]="true"></app-timestamp></ng-template>
 | 
			
		||||
      <ng-template [ngIf]="tx.status.confirmed">‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}</ng-template>
 | 
			
		||||
      <ng-template [ngIf]="!tx.status.confirmed && tx.firstSeen">
 | 
			
		||||
        <i><app-time kind="since" [time]="tx.firstSeen" [fastRender]="true" [showTooltip]="true"></app-time></i>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
@ -81,7 +81,7 @@
 | 
			
		||||
                    </ng-container>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="text-right nowrap amount" [class]="{large: tx.largeInput}">
 | 
			
		||||
                <td class="text-right nowrap amount" [class]="{large: vin?.prevout?.value > 1000000000 || vin.isInscription}">
 | 
			
		||||
                  <button *ngIf="vin.isInscription" (click)="toggleOrdData(tx.txid, 'vin', vindex)" type="button" class="btn btn-sm badge badge-ord primary" style="margin-right: 10px;">Inscription</button>
 | 
			
		||||
                  <ng-template [ngIf]="vin.prevout && vin.prevout.asset && vin.prevout.asset !== nativeAssetId" [ngIfElse]="defaultOutput">
 | 
			
		||||
                    <div *ngIf="assetsMinimal && assetsMinimal[vin.prevout.asset] else assetVinNotFound">
 | 
			
		||||
@ -257,7 +257,7 @@
 | 
			
		||||
                    </ng-template>
 | 
			
		||||
                  </ng-template>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="text-right nowrap amount" [class]="{large: tx.largeOutput}">
 | 
			
		||||
                <td class="text-right nowrap amount" [class]="{large: vout?.value > 1000000000}">
 | 
			
		||||
                  <ng-template [ngIf]="vout.asset && vout.asset !== nativeAssetId" [ngIfElse]="defaultOutput">
 | 
			
		||||
                    <div *ngIf="assetsMinimal && assetsMinimal[vout.asset] else assetNotFound">
 | 
			
		||||
                      <ng-container *ngTemplateOutlet="assetBox; context:{ $implicit: vout }"></ng-container>
 | 
			
		||||
 | 
			
		||||
@ -202,12 +202,12 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
            for (const address of this.addresses) {
 | 
			
		||||
              switch (address.length) {
 | 
			
		||||
                case 130: {
 | 
			
		||||
                  if (v.scriptpubkey === '41' + address + 'ac') {
 | 
			
		||||
                  if (v.scriptpubkey === '21' + address + 'ac') {
 | 
			
		||||
                    return v.value;
 | 
			
		||||
                  }
 | 
			
		||||
                } break;
 | 
			
		||||
                case 66: {
 | 
			
		||||
                  if (v.scriptpubkey === '21' + address + 'ac') {
 | 
			
		||||
                  if (v.scriptpubkey === '41' + address + 'ac') {
 | 
			
		||||
                    return v.value;
 | 
			
		||||
                  }
 | 
			
		||||
                } break;
 | 
			
		||||
@ -224,12 +224,12 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
            for (const address of this.addresses) {
 | 
			
		||||
              switch (address.length) {
 | 
			
		||||
                case 130: {
 | 
			
		||||
                  if (v.prevout?.scriptpubkey === '41' + address + 'ac') {
 | 
			
		||||
                  if (v.prevout?.scriptpubkey === '21' + address + 'ac') {
 | 
			
		||||
                    return v.prevout?.value;
 | 
			
		||||
                  }
 | 
			
		||||
                } break;
 | 
			
		||||
                case 66: {
 | 
			
		||||
                  if (v.prevout?.scriptpubkey === '21' + address + 'ac') {
 | 
			
		||||
                  if (v.prevout?.scriptpubkey === '41' + address + 'ac') {
 | 
			
		||||
                    return v.prevout?.value;
 | 
			
		||||
                  }
 | 
			
		||||
                } break;
 | 
			
		||||
@ -258,7 +258,6 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
              const hasAnnex = tx.vin[i].witness?.[tx.vin[i].witness.length - 1].startsWith('50');
 | 
			
		||||
              if (tx.vin[i].witness.length > (hasAnnex ? 2 : 1) && tx.vin[i].witness[tx.vin[i].witness.length - (hasAnnex ? 3 : 2)].includes('0063036f7264')) {
 | 
			
		||||
                tx.vin[i].isInscription = true;
 | 
			
		||||
                tx.largeInput = true;
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
@ -269,9 +268,6 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        tx.largeInput = tx.largeInput || tx.vin.some(vin => (vin?.prevout?.value > 1000000000));
 | 
			
		||||
        tx.largeOutput = tx.vout.some(vout => (vout?.value > 1000000000));
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (this.blockTime && this.transactions?.length && this.currency) {
 | 
			
		||||
@ -355,12 +351,8 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
      this.electrsApiService.getTransaction$(tx.txid)
 | 
			
		||||
        .subscribe((newTx) => {
 | 
			
		||||
          tx['@vinLoaded'] = true;
 | 
			
		||||
          let temp = tx.vin;
 | 
			
		||||
          tx.vin = newTx.vin;
 | 
			
		||||
          tx.fee = newTx.fee;
 | 
			
		||||
          for (const [index, vin] of temp.entries()) {
 | 
			
		||||
            newTx.vin[index].isInscription = vin.isInscription;
 | 
			
		||||
          }
 | 
			
		||||
          this.ref.markForCheck();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
<a href="#" (click)="twitterLogin()"
 | 
			
		||||
  [class]="(disabled ? 'disabled': '') + (customClass ? customClass : 'w-100 btn mt-1 d-flex justify-content-center align-items-center')"
 | 
			
		||||
  style="background-color: rgb(31, 35, 40)" [style]="width ? 'width: ' + width : ''">
 | 
			
		||||
  style="background-color: #1DA1F2" [style]="width ? 'width: ' + width : ''">
 | 
			
		||||
  <img src="./resources/twitter.svg" height="25" style="padding: 2px" [alt]="buttonString + ' with Twitter'" />
 | 
			
		||||
  <span class="ml-2 text-light align-middle">{{ buttonString }}</span>
 | 
			
		||||
  <img src="./resources/x.svg" height="25" style="padding: 2px; padding-left: 5px" [alt]="buttonString + ' with Twitter'" />
 | 
			
		||||
</a>
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
<div class="container-xl" [class.liquid-address]="network === 'liquid' || network === 'liquidtestnet'">
 | 
			
		||||
  <div class="title-address">
 | 
			
		||||
    <h1>{{ walletName }}</h1>
 | 
			
		||||
    <h1 i18n="shared.wallet">Wallet</h1>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="clearfix"></div>
 | 
			
		||||
@ -74,36 +74,6 @@
 | 
			
		||||
    </ng-container>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
 | 
			
		||||
  <br>
 | 
			
		||||
 | 
			
		||||
  <div class="title-tx">
 | 
			
		||||
    <h2 class="text-left" i18n="address.transactions">Transactions</h2>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <app-transactions-list [transactions]="transactions" [showConfirmations]="true" [addresses]="addressStrings" (loadMore)="loadMore()"></app-transactions-list>
 | 
			
		||||
 | 
			
		||||
  <div class="text-center">
 | 
			
		||||
    <ng-template [ngIf]="isLoadingTransactions">
 | 
			
		||||
      <div class="header-bg box">
 | 
			
		||||
        <div class="row" style="height: 107px;">
 | 
			
		||||
          <div class="col-sm">
 | 
			
		||||
            <span class="skeleton-loader"></span>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="col-sm">
 | 
			
		||||
            <span class="skeleton-loader"></span>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
    </ng-template>
 | 
			
		||||
 | 
			
		||||
    <ng-template [ngIf]="retryLoadMore">
 | 
			
		||||
      <br>
 | 
			
		||||
      <button type="button" class="btn btn-outline-info btn-sm" (click)="loadMore()"><fa-icon [icon]="['fas', 'redo-alt']" [fixedWidth]="true"></fa-icon></button>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  <ng-template #loadingTemplate>
 | 
			
		||||
 | 
			
		||||
    <div class="box" *ngIf="!error; else errorTemplate">
 | 
			
		||||
 | 
			
		||||
@ -9,8 +9,6 @@ import { of, Observable, Subscription } from 'rxjs';
 | 
			
		||||
import { SeoService } from '@app/services/seo.service';
 | 
			
		||||
import { seoDescriptionNetwork } from '@app/shared/common.utils';
 | 
			
		||||
import { WalletAddress } from '@interfaces/node-api.interface';
 | 
			
		||||
import { ElectrsApiService } from '@app/services/electrs-api.service';
 | 
			
		||||
import { AudioService } from '@app/services/audio.service';
 | 
			
		||||
 | 
			
		||||
class WalletStats implements ChainStats {
 | 
			
		||||
  addresses: string[];
 | 
			
		||||
@ -26,7 +24,6 @@ class WalletStats implements ChainStats {
 | 
			
		||||
        acc.funded_txo_sum += stat.funded_txo_sum;
 | 
			
		||||
        acc.spent_txo_count += stat.spent_txo_count;
 | 
			
		||||
        acc.spent_txo_sum += stat.spent_txo_sum;
 | 
			
		||||
        acc.tx_count += stat.tx_count;
 | 
			
		||||
        return acc;
 | 
			
		||||
      }, {
 | 
			
		||||
        funded_txo_count: 0,
 | 
			
		||||
@ -112,17 +109,12 @@ export class WalletComponent implements OnInit, OnDestroy {
 | 
			
		||||
  addressStrings: string[] = [];
 | 
			
		||||
  walletName: string;
 | 
			
		||||
  isLoadingWallet = true;
 | 
			
		||||
  isLoadingTransactions = true;
 | 
			
		||||
  transactions: Transaction[];
 | 
			
		||||
  totalTransactionCount: number;
 | 
			
		||||
  retryLoadMore = false;
 | 
			
		||||
  wallet$: Observable<Record<string, WalletAddress>>;
 | 
			
		||||
  walletAddresses$: Observable<Record<string, Address>>;
 | 
			
		||||
  walletSummary$: Observable<AddressTxSummary[]>;
 | 
			
		||||
  walletStats$: Observable<WalletStats>;
 | 
			
		||||
  error: any;
 | 
			
		||||
  walletSubscription: Subscription;
 | 
			
		||||
  transactionSubscription: Subscription;
 | 
			
		||||
 | 
			
		||||
  collapseAddresses: boolean = true;
 | 
			
		||||
 | 
			
		||||
@ -137,8 +129,6 @@ export class WalletComponent implements OnInit, OnDestroy {
 | 
			
		||||
    private websocketService: WebsocketService,
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private electrsApiService: ElectrsApiService,
 | 
			
		||||
    private audioService: AudioService,
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
@ -182,21 +172,6 @@ export class WalletComponent implements OnInit, OnDestroy {
 | 
			
		||||
      }),
 | 
			
		||||
      switchMap(initial => this.stateService.walletTransactions$.pipe(
 | 
			
		||||
        startWith(null),
 | 
			
		||||
        tap((transactions) => {
 | 
			
		||||
          if (!transactions?.length) {
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          for (const transaction of transactions) {
 | 
			
		||||
            const tx = this.transactions.find((t) => t.txid === transaction.txid);
 | 
			
		||||
            if (tx) {
 | 
			
		||||
              tx.status = transaction.status;
 | 
			
		||||
            } else {
 | 
			
		||||
              this.transactions.unshift(transaction);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          this.transactions = this.transactions.slice();
 | 
			
		||||
          this.audioService.playSound('magic');
 | 
			
		||||
        }),
 | 
			
		||||
        scan((wallet, walletTransactions) => {
 | 
			
		||||
          for (const tx of (walletTransactions || [])) {
 | 
			
		||||
            const funded: Record<string, number> = {};
 | 
			
		||||
@ -292,57 +267,8 @@ export class WalletComponent implements OnInit, OnDestroy {
 | 
			
		||||
            return stats;
 | 
			
		||||
          }, walletStats),
 | 
			
		||||
        );
 | 
			
		||||
      })
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.transactionSubscription = this.wallet$.pipe(
 | 
			
		||||
      switchMap(wallet => {
 | 
			
		||||
        const addresses = Object.keys(wallet).map(addr => this.normalizeAddress(addr));
 | 
			
		||||
        return this.electrsApiService.getAddressesTransactions$(addresses);
 | 
			
		||||
      }),
 | 
			
		||||
      map(transactions => {
 | 
			
		||||
        // only confirmed transactions supported for now
 | 
			
		||||
        return transactions.filter(tx => tx.status.confirmed).sort((a, b) => b.status.block_height - a.status.block_height);
 | 
			
		||||
      }),
 | 
			
		||||
      catchError((error) => {
 | 
			
		||||
        console.log(error);
 | 
			
		||||
        this.error = error;
 | 
			
		||||
        this.seoService.logSoft404();
 | 
			
		||||
        this.isLoadingWallet = false;
 | 
			
		||||
        return of([]);
 | 
			
		||||
      })
 | 
			
		||||
    ).subscribe((transactions: Transaction[] | null) => {
 | 
			
		||||
      if (!transactions) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      this.transactions = transactions;
 | 
			
		||||
      this.isLoadingTransactions = false;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  loadMore(): void {
 | 
			
		||||
    if (this.isLoadingTransactions || this.fullyLoaded) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.isLoadingTransactions = true;
 | 
			
		||||
    this.retryLoadMore = false;
 | 
			
		||||
    this.electrsApiService.getAddressesTransactions$(this.addressStrings, this.transactions[this.transactions.length - 1].txid)
 | 
			
		||||
      .subscribe((transactions: Transaction[]) => {
 | 
			
		||||
        if (transactions && transactions.length) {
 | 
			
		||||
          this.transactions = this.transactions.concat(transactions.sort((a, b) => b.status.block_height - a.status.block_height));
 | 
			
		||||
        } else {
 | 
			
		||||
          this.fullyLoaded = true;
 | 
			
		||||
        }
 | 
			
		||||
        this.isLoadingTransactions = false;
 | 
			
		||||
      },
 | 
			
		||||
      (error) => {
 | 
			
		||||
        this.isLoadingTransactions = false;
 | 
			
		||||
        this.retryLoadMore = true;
 | 
			
		||||
        // In the unlikely event of the txid wasn't found in the mempool anymore and we must reload the page.
 | 
			
		||||
        if (error.status === 422) {
 | 
			
		||||
          window.location.reload();
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  deduplicateWalletTransactions(walletTransactions: AddressTxSummary[]): AddressTxSummary[] {
 | 
			
		||||
@ -373,6 +299,5 @@ export class WalletComponent implements OnInit, OnDestroy {
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    this.websocketService.stopTrackingWallet();
 | 
			
		||||
    this.walletSubscription.unsubscribe();
 | 
			
		||||
    this.transactionSubscription.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,6 +1,6 @@
 | 
			
		||||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
 | 
			
		||||
import { Env, StateService } from '@app/services/state.service';
 | 
			
		||||
import { restApiDocsData, wsApiDocsData } from '@app/docs/api-docs/api-docs-data';
 | 
			
		||||
import { restApiDocsData } from '@app/docs/api-docs/api-docs-data';
 | 
			
		||||
import { faqData } from '@app/docs/api-docs/api-docs-data';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
@ -28,8 +28,6 @@ export class ApiDocsNavComponent implements OnInit {
 | 
			
		||||
    this.auditEnabled = this.env.AUDIT;
 | 
			
		||||
    if (this.whichTab === 'rest') {
 | 
			
		||||
      this.tabData = restApiDocsData;
 | 
			
		||||
    } else if (this.whichTab === 'websocket') {
 | 
			
		||||
      this.tabData = wsApiDocsData;
 | 
			
		||||
    } else if (this.whichTab === 'faq') {
 | 
			
		||||
      this.tabData = faqData;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -108,43 +108,18 @@
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div id="websocketAPI" *ngIf="whichTab === 'websocket'">
 | 
			
		||||
 | 
			
		||||
      <div id="doc-nav-desktop" class="hide-on-mobile" [ngClass]="desktopDocsNavPosition">
 | 
			
		||||
        <app-api-docs-nav (navLinkClickEvent)="anchorLinkClick( $event )" [network]="{ val: network$ | async }" [whichTab]="whichTab"></app-api-docs-nav>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="doc-content">
 | 
			
		||||
 | 
			
		||||
        <div id="enterprise-cta-mobile" *ngIf="officialMempoolInstance && showMobileEnterpriseUpsell">
 | 
			
		||||
          <p>Get higher API limits with <span class="no-line-break">Mempool Enterprise®</span></p>
 | 
			
		||||
          <div class="button-group">
 | 
			
		||||
            <a class="btn btn-small btn-secondary" (click)="showMobileEnterpriseUpsell = false">No Thanks</a>
 | 
			
		||||
            <a class="btn btn-small btn-purple" href="https://mempool.space/enterprise">More Info <fa-icon [icon]="['fas', 'angle-right']" [styles]="{'font-size': '12px'}"></fa-icon></a>
 | 
			
		||||
    <div id="websocketAPI" *ngIf="( whichTab === 'websocket' )">
 | 
			
		||||
      <div class="api-category">
 | 
			
		||||
        <div class="websocket">
 | 
			
		||||
          <div class="endpoint">
 | 
			
		||||
            <div class="subtitle" i18n="Api docs endpoint">Endpoint</div>
 | 
			
		||||
            {{ wrapUrl(network.val, wsDocs, true) }}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <p class="doc-welcome-note">Below is a reference for the {{ network.val === '' ? 'Bitcoin' : network.val.charAt(0).toUpperCase() + network.val.slice(1) }} <ng-container i18n="api-docs.title-websocket">Websocket service</ng-container> running at {{ websocketUrl(network.val) }}.</p>
 | 
			
		||||
        <p class="doc-welcome-note api-note" *ngIf="officialMempoolInstance">Note that usage limits apply to our WebSocket API. Consider an <a href="https://mempool.space/enterprise">enterprise sponsorship</a> if you need higher API limits, such as higher tracking limits.</p>
 | 
			
		||||
 | 
			
		||||
        <div class="doc-item-container" *ngFor="let item of wsDocs">
 | 
			
		||||
          <div *ngIf="!item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('officialOnly') && item.options.officialOnly && officialMempoolInstance )">
 | 
			
		||||
            <h3 *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )">{{ item.title }}</h3>
 | 
			
		||||
            <div *ngIf="( item.type !== 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )" class="endpoint-container" id="{{ item.fragment }}">
 | 
			
		||||
              <a id="{{ item.fragment + '-tab-header' }}" class="section-header" (click)="anchorLinkClick({event: $event, fragment: item.fragment})">{{ item.title }} <span>{{ item.category }}</span></a>
 | 
			
		||||
              <div class="endpoint-content">
 | 
			
		||||
                <div class="description">
 | 
			
		||||
                  <div class="subtitle" i18n>Description</div>
 | 
			
		||||
                  <div [innerHTML]="item.description.default" i18n></div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="description">
 | 
			
		||||
                  <div class="subtitle" i18n>Payload</div>
 | 
			
		||||
                  <pre><code [innerText]="item.payload"></code></pre>
 | 
			
		||||
                </div>
 | 
			
		||||
                <app-code-template [hostname]="hostname" [baseNetworkUrl]="baseNetworkUrl" [method]="item.httpRequestMethod" [code]="item.codeExample.default" [network]="network.val" [showCodeExample]="item.showJsExamples"></app-code-template>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          <div class="description">
 | 
			
		||||
            <div class="subtitle" i18n>Description</div>
 | 
			
		||||
            <div i18n="api-docs.websocket.websocket">Default push: <code>{{ '{' }} action: 'want', data: ['blocks', ...] {{ '}' }}</code> to express what you want pushed. Available: <code>blocks</code>, <code>mempool-blocks</code>, <code>live-2h-chart</code>, and <code>stats</code>.<br><br>Push transactions related to address: <code>{{ '{' }} 'track-address': '3PbJ...bF9B' {{ '}' }}</code> to receive all new transactions containing that address as input or output. Returns an array of transactions. <code>address-transactions</code> for new mempool transactions, and <code>block-transactions</code> for new block confirmed transactions.</div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <app-code-template [method]="'websocket'" [hostname]="hostname" [code]="wsDocs" [network]="network.val" [showCodeExample]="wsDocs.showJsExamples"></app-code-template>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@ -470,21 +470,3 @@ dd {
 | 
			
		||||
    margin-left: 1em;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
code {
 | 
			
		||||
  background-color: var(--bg);
 | 
			
		||||
  font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pre {
 | 
			
		||||
  display: block;
 | 
			
		||||
  font-size: 87.5%;
 | 
			
		||||
  color: #f18920;
 | 
			
		||||
  background-color: var(--bg);
 | 
			
		||||
  padding: 30px;
 | 
			
		||||
  code{
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
    white-space: break-spaces;
 | 
			
		||||
    word-break: break-all;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -145,7 +145,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
 | 
			
		||||
    if (document.getElementById( targetId + "-tab-header" )) {
 | 
			
		||||
      tabHeaderHeight = document.getElementById( targetId + "-tab-header" ).scrollHeight;
 | 
			
		||||
    }
 | 
			
		||||
    if( ( window.innerWidth <= 992 ) && ( ( this.whichTab === 'rest' ) || ( this.whichTab === 'faq' ) || ( this.whichTab === 'websocket' ) ) && targetId ) {
 | 
			
		||||
    if( ( window.innerWidth <= 992 ) && ( ( this.whichTab === 'rest' ) || ( this.whichTab === 'faq' ) ) && targetId ) {
 | 
			
		||||
      const endpointContainerEl = document.querySelector<HTMLElement>( "#" + targetId );
 | 
			
		||||
      const endpointContentEl = document.querySelector<HTMLElement>( "#" + targetId + " .endpoint-content" );
 | 
			
		||||
      const endPointContentElHeight = endpointContentEl.clientHeight;
 | 
			
		||||
@ -207,29 +207,13 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
 | 
			
		||||
      text = text.replace('%{' + indexNumber + '}', curlText);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (websocket) {
 | 
			
		||||
      const wsHostname = this.hostname.replace('https://', 'wss://');
 | 
			
		||||
      wsHostname.replace('http://', 'ws://');
 | 
			
		||||
      return `${wsHostname}${curlNetwork}${text}`;
 | 
			
		||||
    }
 | 
			
		||||
    return `${this.hostname}${curlNetwork}${text}`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  websocketUrl(network: string) {
 | 
			
		||||
    let curlNetwork = '';
 | 
			
		||||
    if (this.env.BASE_MODULE === 'mempool') {
 | 
			
		||||
      if (!['', 'mainnet'].includes(network)) {
 | 
			
		||||
        curlNetwork = `/${network}`;
 | 
			
		||||
      }
 | 
			
		||||
    } else if (this.env.BASE_MODULE === 'liquid') {
 | 
			
		||||
      if (!['', 'liquid'].includes(network)) {
 | 
			
		||||
        curlNetwork = `/${network}`;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (network === this.env.ROOT_NETWORK) {
 | 
			
		||||
      curlNetwork = '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let wsHostname = this.hostname.replace('https://', 'wss://');
 | 
			
		||||
    wsHostname = wsHostname.replace('http://', 'ws://');
 | 
			
		||||
    return `${wsHostname}${curlNetwork}/api/v1/ws`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -32,8 +32,6 @@ export interface Transaction {
 | 
			
		||||
  price?: Price;
 | 
			
		||||
  sigops?: number;
 | 
			
		||||
  flags?: bigint;
 | 
			
		||||
  largeInput?: boolean;
 | 
			
		||||
  largeOutput?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TransactionChannels {
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { AddressTxSummary, Block, ChainStats } from "./electrs.interface";
 | 
			
		||||
import { AddressTxSummary, Block, ChainStats, Transaction } from "./electrs.interface";
 | 
			
		||||
 | 
			
		||||
export interface OptimizedMempoolStats {
 | 
			
		||||
  added: number;
 | 
			
		||||
@ -412,13 +412,13 @@ export interface Acceleration {
 | 
			
		||||
  feeDelta: number;
 | 
			
		||||
  blockHash: string;
 | 
			
		||||
  blockHeight: number;
 | 
			
		||||
 | 
			
		||||
  acceleratedFeeRate?: number;
 | 
			
		||||
  boost?: number;
 | 
			
		||||
  bidBoost?: number;
 | 
			
		||||
  boostCost?: number;
 | 
			
		||||
  boostRate?: number;
 | 
			
		||||
  minedByPoolUniqueId?: number;
 | 
			
		||||
  canceled?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AccelerationHistoryParams {
 | 
			
		||||
 | 
			
		||||
@ -21,8 +21,6 @@ export interface WebsocketResponse {
 | 
			
		||||
  rbfInfo?: RbfTree;
 | 
			
		||||
  rbfLatest?: RbfTree[];
 | 
			
		||||
  rbfLatestSummary?: ReplacementInfo[];
 | 
			
		||||
  stratumJob?: StratumJob;
 | 
			
		||||
  stratumJobs?: Record<number, StratumJob>;
 | 
			
		||||
  utxoSpent?: object;
 | 
			
		||||
  transactions?: TransactionStripped[];
 | 
			
		||||
  loadingIndicators?: ILoadingIndicators;
 | 
			
		||||
@ -39,7 +37,6 @@ export interface WebsocketResponse {
 | 
			
		||||
  'track-rbf-summary'?: boolean;
 | 
			
		||||
  'track-accelerations'?: boolean;
 | 
			
		||||
  'track-wallet'?: string;
 | 
			
		||||
  'track-stratum'?: string | number;
 | 
			
		||||
  'watch-mempool'?: boolean;
 | 
			
		||||
  'refresh-blocks'?: boolean;
 | 
			
		||||
}
 | 
			
		||||
@ -147,30 +144,4 @@ export interface HealthCheckHost {
 | 
			
		||||
  link?: string;
 | 
			
		||||
  statusPage?: SafeResourceUrl;
 | 
			
		||||
  flag?: string;
 | 
			
		||||
  hashes?: {
 | 
			
		||||
    frontend?: string;
 | 
			
		||||
    backend?: 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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,7 @@
 | 
			
		||||
        <tbody>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td i18n="lightning.created">Created</td>
 | 
			
		||||
            <td><app-timestamp [customFormat]="'yyyy-MM-dd HH:mm'" [unixTime]="channel.created" [hideTimeSince]="true"></app-timestamp></td>
 | 
			
		||||
            <td>{{ channel.created | date:'yyyy-MM-dd HH:mm' }}</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td i18n="lightning.capacity">Capacity</td>
 | 
			
		||||
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user