Compare commits

...

117 Commits

Author SHA1 Message Date
Kgothatso
a4e536d637 chimaera logic 2025-02-01 12:20:02 +02:00
Kgothatso
a232b6d3e8 Oh well 2025-02-01 12:09:04 +02:00
Kgothatso
4bcb3f6ab2 Configure the mempool backend 2025-02-01 12:06:58 +02:00
Kgothatso
6f8901229a Don't run docker on port 80 2025-02-01 12:04:55 +02:00
wiz
bb5b771128
ops: Tweak nginx cache limits for apinormal 2025-02-01 01:52:19 +09:00
wiz
6a17d665e0
Merge pull request #5761 from mempool/nymkappa/cashapp-69 2025-01-29 12:19:18 +09:00
nymkappa
f54bccf267
[accelerator] fix cashapp acceleration 2025-01-28 18:33:50 +01:00
wiz
f909d6bca5
Merge pull request #5760 from mempool/natsoni/enable-first-seen 2025-01-28 21:01:07 +09:00
natsoni
2407cbfd9a
Enable microsecond logging in bitcoin.conf 2025-01-28 12:55:28 +01:00
natsoni
ec7af86142
Add debug.log path to mainnet prod config 2025-01-28 12:47:11 +01:00
wiz
5f761098ae
Merge pull request #5753 from mempool/knorrium/update_rust 2025-01-28 18:58:45 +09:00
wiz
b2456d104c
Merge pull request #5759 from mempool/nymkappa/image-md5 2025-01-28 18:56:50 +09:00
nymkappa
edc5593792
[services] remove image md5 in urls 2025-01-28 07:57:19 +01:00
wiz
85c78a448d
Merge pull request #5755 from mempool/mononaut/hnl 2025-01-27 12:52:55 +09:00
Mononaut
60d548df46
add missing sg1 hnl emojis 2025-01-27 03:37:01 +00:00
Felipe Knorr Kuhn
0674f3a3ee
Update Rust to 1.84 2025-01-26 10:05:19 -08:00
Felipe Knorr Kuhn
210b632720
Merge branch 'master' into knorrium/update_rust 2025-01-25 19:09:22 -08:00
wiz
fc6c97172b
Merge pull request #5754 from mempool/nymkappa/cc-icons
[accelerator] add credit card provider fa icons
2025-01-26 11:29:45 +09:00
nymkappa
a074c4b2c3
[accelerator] add credit card provider fa icons 2025-01-26 11:26:27 +09:00
Felipe Knorr Kuhn
9e8a35f4a9
Update rust to 1.83 2025-01-25 18:00:45 -08:00
wiz
7b581b2ac3
Merge pull request #5691 from mempool/ops/remove-fmt-add-hnl-sg1
ops: Remove fmt, add hnl + sg1
2025-01-25 17:59:11 +09:00
wiz
1a62c867de
Merge branch 'master' into ops/remove-fmt-add-hnl-sg1 2025-01-25 17:57:28 +09:00
wiz
470a6a7534
Merge pull request #5752 from mempool/nymkappa/rename-twitter-x
[services] twitter -> X
2025-01-25 17:50:53 +09:00
wiz
8bd7849b9d
Merge pull request #5721 from mempool/nymkappa/retry-btcpay-invoice
[accelerator] add btcpay invoice retry button and polish checkout UI
2025-01-25 16:57:39 +09:00
wiz
43393f7227
Merge pull request #5705 from mempool/nymkappa/503-faucet-utxo-chain
[faucet] add new error message when no utxo available
2025-01-25 16:54:43 +09:00
wiz
f67f946723
Merge pull request #5626 from mempool/nymkappa/remove-tx-restrictions-cashapp
[accelerator] remove tx restriction for cashapp payments
2025-01-25 16:54:23 +09:00
wiz
a0d0ee230e
Merge pull request #5665 from mempool/nymkappa/block-pools-json-hash
[blocks] fetch list of block hash filtered by pools-v2.json sha
2025-01-25 16:51:36 +09:00
wiz
edf7798587
Merge pull request #5436 from mempool/nymkappa/fix-bitcoin-payment
[btcpay] better handling for invoice expiration
2025-01-25 16:37:04 +09:00
nymkappa
b826227b36
[services] twitter -> X 2025-01-25 15:46:21 +09:00
wiz
0924f6184b
Merge pull request #5751 from mempool/mononaut/fix-merkle-row-layout 2025-01-24 12:52:20 +09:00
Mononaut
24d0ed4ced
fix next block merkle row layout 2025-01-24 01:56:15 +00:00
nymkappa
9eb85200e0
Merge branch 'master' into nymkappa/fix-bitcoin-payment 2025-01-24 10:53:29 +09:00
wiz
a79c165b30
Merge pull request #5749 from mempool/nymkappa/fido 2025-01-23 22:44:00 +09:00
wiz
7f07c5cbab
Merge branch 'master' into nymkappa/fido 2025-01-23 21:14:35 +09:00
wiz
c021a15d0b
Merge pull request #5750 from mempool/nymkappa/fix-sca-card-on-file 2025-01-23 21:14:24 +09:00
Felipe Knorr Kuhn
a2009aa322
Merge branch 'master' into nymkappa/fido 2025-01-23 01:08:25 -08:00
nymkappa
e6965dac80
[accelerator] fix sca card on file 2025-01-23 18:00:50 +09:00
wiz
c4a7a2e781
Merge pull request #5732 from mempool/nymkappa/accelerator-card-on-file
[accelerator] add support for card on file acceleration
2025-01-23 16:43:14 +09:00
wiz
03bea14f89
Merge pull request #5747 from mempool/nymkappa/overflow
[accelerator] truncate dashboard title
2025-01-23 16:42:44 +09:00
wiz
834f9a9f6d
Merge pull request #5748 from mempool/knorrium/fix_docker_frontend_stratum_config
Fix the missing frontend Stratum config for Docker builds
2025-01-23 16:42:23 +09:00
nymkappa
518297494f
Merge branch 'master' into nymkappa/accelerator-card-on-file 2025-01-23 16:18:40 +09:00
nymkappa
15e67bc77f
Merge branch 'master' into nymkappa/fido 2025-01-23 15:30:26 +09:00
wiz
ff383f9c58
Merge pull request #5742 from mempool/nymkappa/faucet-twitter-to-github
[auth] add login/signup with github support
2025-01-23 15:21:34 +09:00
Felipe Knorr Kuhn
ed44e27991
Merge branch 'master' into knorrium/fix_docker_frontend_stratum_config 2025-01-22 22:01:53 -08:00
wiz
a869ea5ec4
Merge pull request #5746 from mempool/nymkappa/failed-canceled-accel
[accelerator] differentiate failed/canceled accelerations
2025-01-23 14:59:23 +09:00
Felipe Knorr Kuhn
fc4a0f7461
Fix the missing frontend Stratum config for Docker builds 2025-01-22 21:59:15 -08:00
nymkappa
b3f21a10b9
[accelerator] truncate dashboard title 2025-01-23 14:55:31 +09:00
wiz
4290e00376
Merge pull request #5745 from mempool/mononaut/stratum-tweaks 2025-01-22 21:11:33 +09:00
nymkappa
3b91a1437a
[accelerator] differentiate failed/canceled accelerations 2025-01-22 18:58:54 +09:00
Mononaut
363fa3d877
improve stratum table layout on mobile 2025-01-22 09:16:25 +00:00
Mononaut
2e44ea3f01
reorder stratum job table 2025-01-22 09:13:44 +00:00
Mononaut
cac62765a1
fix stratum tree branch level 2025-01-22 09:10:22 +00:00
Mononaut
23713a11c2
link to merkle branch first tx 2025-01-22 05:06:02 +00:00
nymkappa
ff4aca8370
[blocks] update sha in memory/db before re-indexing blocks 2025-01-22 11:26:34 +09:00
softsimon
469faf7456
Merge pull request #5707 from mempool/dependabot/npm_and_yarn/backend/mysql2-3.12.0
Bump mysql2 from 3.11.0 to 3.12.0 in /backend
2025-01-21 23:18:11 +07:00
dependabot[bot]
665a12a040
Bump mysql2 from 3.11.0 to 3.12.0 in /backend
Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.11.0 to 3.12.0.
- [Release notes](https://github.com/sidorares/node-mysql2/releases)
- [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md)
- [Commits](https://github.com/sidorares/node-mysql2/compare/v3.11.0...v3.12.0)

---
updated-dependencies:
- dependency-name: mysql2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-21 15:57:18 +00:00
nymkappa
b42431f14a
[auth] add login/signup with github support 2025-01-21 17:39:50 +09:00
softsimon
69dfcbe6fa
Merge pull request #5741 from mempool/simon/remove-babel-dep
Remove babel backend dep
2025-01-21 14:09:39 +07:00
softsimon
b454fa09d2
Remove babel backend dep 2025-01-21 13:49:12 +07:00
softsimon
019101862f
Merge pull request #5711 from mempool/dependabot/npm_and_yarn/frontend/echarts-5.6.0
Bump echarts from 5.5.0 to 5.6.0 in /frontend
2025-01-21 13:43:09 +07:00
wiz
5aeaa68259
ops: Enable stratum in FOSS prod frontend config 2025-01-20 17:56:56 +09:00
nymkappa
e01898a4c5
Merge pull request #5640 from mempool/nymkappa/square-errors
[accelerator] display payment errors, auto reload after 10 secs instead of 3 secs
2025-01-20 17:47:18 +09:00
nymkappa
817076fcbd
Merge branch 'master' into nymkappa/accelerator-card-on-file 2025-01-20 17:36:36 +09:00
nymkappa
0568a8c6c1
Merge branch 'master' into nymkappa/square-errors 2025-01-20 17:36:31 +09:00
wiz
e53e810a55
ops: Fix stratum server URL path in prod config 2025-01-20 17:34:19 +09:00
nymkappa
e2c44b6c62
Merge branch 'master' into nymkappa/square-errors 2025-01-20 17:23:15 +09:00
wiz
36b691e25b
ops: Enable stratum in prod config via localhost nginx proxy 2025-01-20 17:20:38 +09:00
nymkappa
778837322d
Merge branch 'master' into nymkappa/accelerator-card-on-file 2025-01-20 17:05:55 +09:00
wiz
4a14e8d921
Merge pull request #5736 from mempool/mononaut/fix-stratum-trees
fix stratum tree rendering with different branch lengths
2025-01-20 16:39:56 +09:00
Mononaut
4e735cc8b0
fix stratum tree rendering with different branch lengths 2025-01-20 07:30:27 +00:00
softsimon
4520e3fdf2
Merge pull request #5735 from mempool/nymkappa/new-fa-icon
add new fa icon
2025-01-20 14:01:37 +07:00
nymkappa
390bbf1097
add new fa icon 2025-01-20 15:20:29 +09:00
wiz
bd8c1efc8e
Merge pull request #5734 from mempool/knorrium/update_stg_hosts 2025-01-20 13:07:31 +09:00
Felipe Knorr Kuhn
8fbc497a58
Merge branch 'knorrium/update_stg_hosts' of github.com:mempool/mempool into knorrium/update_stg_hosts 2025-01-19 19:50:25 -08:00
Felipe Knorr Kuhn
003956fd16
Update staging hosts
Add rerouting logic for testnet4 tests
Split CI e2e workflow matrix into mempool, liquid and testnet4
2025-01-19 19:50:05 -08:00
Felipe Knorr Kuhn
227d99e990
Fix reroute logic 2025-01-19 19:35:08 -08:00
Felipe Knorr Kuhn
3d1aacbd66
Copypasta matrix for tests 2025-01-19 19:21:02 -08:00
Felipe Knorr Kuhn
1098d2fe3c
Change build step to shell script 2025-01-19 14:21:23 -08:00
Felipe Knorr Kuhn
f59e95fcc8
Run the default mempool config if we are running testnet4 tests 2025-01-19 14:11:41 -08:00
Felipe Knorr Kuhn
7f6399093e
Fix YAML 2025-01-19 13:56:16 -08:00
Felipe Knorr Kuhn
e9e8b0c758
Use testnet as a matrix config instead 2025-01-19 13:38:06 -08:00
Felipe Knorr Kuhn
517a30d2b0
Split module and spec matrix 2025-01-19 13:25:29 -08:00
Felipe Knorr Kuhn
7e766cc28d
Change spec test 2025-01-19 12:36:20 -08:00
Felipe Knorr Kuhn
34099e3861
Stop switching to testnet4 until we can check for the proxied server 2025-01-19 12:32:37 -08:00
Felipe Knorr Kuhn
671b5ea2f2
Reroute testnet4 tests to a different server 2025-01-19 12:24:26 -08:00
Felipe Knorr Kuhn
caa2d83247
Update staging hosts 2025-01-19 02:34:05 -08:00
nymkappa
58e6a78579
[accelerator] add support for card on file acceleration 2025-01-19 17:56:19 +09:00
wiz
703241acf0
Merge pull request #5509 from mempool/mononaut/pool-next-block
mining pool next block info
2025-01-19 17:00:38 +09:00
wiz
6e8579363d
Merge pull request #5508 from mempool/mononaut/stratum
stratum job visualization
2025-01-19 16:48:48 +09:00
Mononaut
b254be2f49
add stratum job to pool page 2025-01-19 06:02:58 +00:00
Mononaut
d6283c54ee
fix stratum config 2025-01-19 05:42:40 +00:00
Mononaut
9ba7172b5b
add coinbase tag column to stratum table 2025-01-19 05:42:40 +00:00
Mononaut
cb4bf0611e
add blockchain bar to stratum page 2025-01-19 05:42:40 +00:00
Mononaut
3ea491ad13
stratum frontend config 2025-01-19 05:42:40 +00:00
Mononaut
eddd7344ad
stratum backend config 2025-01-19 05:42:39 +00:00
Mononaut
4ecf2eb679
experimental stratum job visualization 2025-01-19 05:41:48 +00:00
wiz
34acbca4b9
Merge pull request #5720 from mempool/nymkappa/missing-icon
add new robot icon
2025-01-17 17:14:47 +09:00
wiz
8793fafa4c
Merge pull request #5704 from mempool/mononaut/sca-loading-ux
[accelerator] improve SCA UX
2025-01-17 17:04:52 +09:00
nymkappa
3325db4883
[accelerator] add btcpay invoice retry button and polish checkout UI 2025-01-13 11:06:21 +09:00
nymkappa
4a4259fa7d
add new robot icon, wrap import into new lines 2025-01-12 12:16:51 +09:00
nymkappa
faa83866fd
[blocks] fix block pools-v2 hash migration 2025-01-12 12:04:19 +09:00
nymkappa
54058b64ad
Merge branch 'master' into nymkappa/block-pools-json-hash 2025-01-12 11:47:55 +09:00
nymkappa
47cc58c610
Merge branch 'master' into mononaut/sca-loading-ux 2025-01-12 11:11:29 +09:00
nymkappa
21cdb7e3a1
[accelerator] remove tx restriction for cashapp payments 2025-01-11 18:31:45 +09:00
dependabot[bot]
e77dd114f4
Bump echarts from 5.5.0 to 5.6.0 in /frontend
Bumps [echarts](https://github.com/apache/echarts) from 5.5.0 to 5.6.0.
- [Release notes](https://github.com/apache/echarts/releases)
- [Commits](https://github.com/apache/echarts/compare/5.5.0...5.6.0)

---
updated-dependencies:
- dependency-name: echarts
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-30 02:15:20 +00:00
nymkappa
ff235760b2
[fido] logout localstorage update 2024-12-27 15:37:36 +08:00
Mononaut
f49152d09d
[accelerator] keep checkout locked until request completes 2024-12-22 15:34:32 +00:00
Mononaut
464fabf137
[accelerator] reference counting for checkout lock 2024-12-22 15:34:03 +00:00
nymkappa
c9b9485313
[faucet] add new error message when no utxo available 2024-12-22 22:40:35 +08:00
Mononaut
ba1ee15286
[accelerator] improve SCA UX 2024-12-22 12:27:29 +00:00
wiz
8670897a50
ops: Remove fmt, add hnl + sg1 2024-12-14 21:01:37 +09:00
nymkappa
4ff2aad94a
[blocks] return list of block hash filtered by definition hash 2024-11-29 17:55:06 +01:00
nymkappa
ac997f3d9e
[blocks] add 2 endpoints to retreive pools-v2.json hashes 2024-11-29 17:13:28 +01:00
nymkappa
c8e967cc0c
[blocks] save pools-v2.json hash in blocks table 2024-11-29 16:04:54 +01:00
nymkappa
72ddb8c6a4
[accelerator] display payment errors, auto reload after 10 secs instead of 3 secs 2024-11-14 16:46:18 +01:00
nymkappa
cbce49a8bf
Merge branch 'master' into nymkappa/fix-bitcoin-payment 2024-10-25 15:45:16 +09:00
nymkappa
f2e7cf7441
[btcpay] better handling for invoice expiration 2024-10-18 21:49:35 +09:00
79 changed files with 1930 additions and 566 deletions

View File

@ -251,17 +251,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
module: ["mempool", "liquid"] module: ["mempool", "liquid", "testnet4"]
include:
- module: "mempool"
spec: |
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
cypress/e2e/testnet4/*.spec.ts
- module: "liquid"
spec: |
cypress/e2e/liquid/liquid.spec.ts
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
name: E2E tests for ${{ matrix.module }} name: E2E tests for ${{ matrix.module }}
steps: steps:
@ -310,8 +300,10 @@ jobs:
- name: Unzip assets before building (src/resources) - name: Unzip assets before building (src/resources)
run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video
# mempool
- name: Chrome browser tests (${{ matrix.module }}) - name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'mempool' }}
uses: cypress-io/github-action@v5 uses: cypress-io/github-action@v5
with: with:
tag: ${{ github.event_name }} tag: ${{ github.event_name }}
@ -322,7 +314,9 @@ jobs:
wait-on-timeout: 120 wait-on-timeout: 120
record: true record: true
parallel: true parallel: true
spec: ${{ matrix.spec }} spec: |
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
group: Tests on Chrome (${{ matrix.module }}) group: Tests on Chrome (${{ matrix.module }})
browser: "chrome" browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
@ -332,6 +326,56 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
# liquid
- name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'liquid' }}
uses: cypress-io/github-action@v5
with:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:${{ matrix.module }}
start: npm run start:local-staging
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/liquid/liquid.spec.ts
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
env:
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
# testnet
- name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'testnet4' }}
uses: cypress-io/github-action@v5
with:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:mempool
start: npm run start:local-staging
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/testnet4/*.spec.ts
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
env:
CYPRESS_REROUTE_TESTNET: true
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
validate_docker_json: validate_docker_json:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
@ -359,4 +403,4 @@ jobs:
- name: Validate JSON syntax - name: Validate JSON syntax
run: | run: |
cat mempool-config.json | jq cat mempool-config.json | jq
working-directory: docker/docker/backend working-directory: docker/docker/backend

View File

@ -7,7 +7,7 @@ const config: Config.InitialOptions = {
automock: false, automock: false,
collectCoverage: true, collectCoverage: true,
collectCoverageFrom: ["./src/**/**.ts"], collectCoverageFrom: ["./src/**/**.ts"],
coverageProvider: "babel", coverageProvider: "v8",
coverageThreshold: { coverageThreshold: {
global: { global: {
lines: 1 lines: 1

View File

@ -155,6 +155,10 @@
"API": "https://mempool.space/api/v1/services", "API": "https://mempool.space/api/v1/services",
"ACCELERATIONS": false "ACCELERATIONS": false
}, },
"STRATUM": {
"ENABLED": false,
"API": "http://localhost:1234"
},
"FIAT_PRICE": { "FIAT_PRICE": {
"ENABLED": true, "ENABLED": true,
"PAID": false, "PAID": false,

View File

@ -10,7 +10,6 @@
"hasInstallScript": true, "hasInstallScript": true,
"license": "GNU Affero General Public License v3.0", "license": "GNU Affero General Public License v3.0",
"dependencies": { "dependencies": {
"@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9", "@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3", "@types/node": "^18.15.3",
"axios": "1.7.2", "axios": "1.7.2",
@ -18,7 +17,7 @@
"crypto-js": "~4.2.0", "crypto-js": "~4.2.0",
"express": "~4.21.1", "express": "~4.21.1",
"maxmind": "~4.3.11", "maxmind": "~4.3.11",
"mysql2": "~3.11.0", "mysql2": "~3.12.0",
"redis": "^4.7.0", "redis": "^4.7.0",
"rust-gbt": "file:./rust-gbt", "rust-gbt": "file:./rust-gbt",
"socks-proxy-agent": "~7.0.0", "socks-proxy-agent": "~7.0.0",
@ -26,8 +25,6 @@
"ws": "~8.18.0" "ws": "~8.18.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/core": "^7.25.2",
"@types/compression": "^1.7.2", "@types/compression": "^1.7.2",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
@ -6000,6 +5997,21 @@
"yallist": "^3.0.2" "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": { "node_modules/make-dir": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@ -6161,16 +6173,17 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"node_modules/mysql2": { "node_modules/mysql2": {
"version": "3.11.0", "version": "3.12.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.0.tgz", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz",
"integrity": "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA==", "integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==",
"license": "MIT",
"dependencies": { "dependencies": {
"aws-ssl-profiles": "^1.1.1", "aws-ssl-profiles": "^1.1.1",
"denque": "^2.1.0", "denque": "^2.1.0",
"generate-function": "^2.3.1", "generate-function": "^2.3.1",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"long": "^5.2.1", "long": "^5.2.1",
"lru-cache": "^8.0.0", "lru.min": "^1.0.0",
"named-placeholders": "^1.1.3", "named-placeholders": "^1.1.3",
"seq-queue": "^0.0.5", "seq-queue": "^0.0.5",
"sqlstring": "^2.3.2" "sqlstring": "^2.3.2"
@ -6190,14 +6203,6 @@
"node": ">=0.10.0" "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": { "node_modules/named-placeholders": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz",
@ -12213,6 +12218,11 @@
"yallist": "^3.0.2" "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": { "make-dir": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@ -12327,16 +12337,16 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"mysql2": { "mysql2": {
"version": "3.11.0", "version": "3.12.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.0.tgz", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz",
"integrity": "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA==", "integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==",
"requires": { "requires": {
"aws-ssl-profiles": "^1.1.1", "aws-ssl-profiles": "^1.1.1",
"denque": "^2.1.0", "denque": "^2.1.0",
"generate-function": "^2.3.1", "generate-function": "^2.3.1",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"long": "^5.2.1", "long": "^5.2.1",
"lru-cache": "^8.0.0", "lru.min": "^1.0.0",
"named-placeholders": "^1.1.3", "named-placeholders": "^1.1.3",
"seq-queue": "^0.0.5", "seq-queue": "^0.0.5",
"sqlstring": "^2.3.2" "sqlstring": "^2.3.2"
@ -12349,11 +12359,6 @@
"requires": { "requires": {
"safer-buffer": ">= 2.1.2 < 3.0.0" "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=="
} }
} }
}, },

View File

@ -39,7 +39,6 @@
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"" "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9", "@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3", "@types/node": "^18.15.3",
"axios": "1.7.2", "axios": "1.7.2",
@ -47,7 +46,7 @@
"crypto-js": "~4.2.0", "crypto-js": "~4.2.0",
"express": "~4.21.1", "express": "~4.21.1",
"maxmind": "~4.3.11", "maxmind": "~4.3.11",
"mysql2": "~3.11.0", "mysql2": "~3.12.0",
"rust-gbt": "file:./rust-gbt", "rust-gbt": "file:./rust-gbt",
"redis": "^4.7.0", "redis": "^4.7.0",
"socks-proxy-agent": "~7.0.0", "socks-proxy-agent": "~7.0.0",
@ -55,8 +54,6 @@
"ws": "~8.18.0" "ws": "~8.18.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/core": "^7.25.2",
"@types/compression": "^1.7.2", "@types/compression": "^1.7.2",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",

View File

@ -151,5 +151,9 @@
"ENABLED": true, "ENABLED": true,
"PAID": false, "PAID": false,
"API_KEY": "__MEMPOOL_CURRENCY_API_KEY__" "API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
},
"STRATUM": {
"ENABLED": false,
"API": "http://localhost:1234"
} }
} }

View File

@ -159,6 +159,11 @@ describe('Mempool Backend Config', () => {
PAID: false, PAID: false,
API_KEY: '', API_KEY: '',
}); });
expect(config.STRATUM).toStrictEqual({
ENABLED: false,
API: 'http://localhost:1234',
});
}); });
}); });

View File

@ -21,6 +21,7 @@ import transactionRepository from '../../repositories/TransactionRepository';
import rbfCache from '../rbf-cache'; import rbfCache from '../rbf-cache';
import { calculateMempoolTxCpfp } from '../cpfp'; import { calculateMempoolTxCpfp } from '../cpfp';
import { handleError } from '../../utils/api'; import { handleError } from '../../utils/api';
import poolsUpdater from '../../tasks/pools-updater';
const TXID_REGEX = /^[a-f0-9]{64}$/i; const TXID_REGEX = /^[a-f0-9]{64}$/i;
const BLOCK_HASH_REGEX = /^[a-f0-9]{64}$/i; const BLOCK_HASH_REGEX = /^[a-f0-9]{64}$/i;
@ -56,6 +57,10 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this)) .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 // Temporarily add txs/package endpoint for all backends until esplora supports it
.post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage) .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') { if (config.MEMPOOL.BACKEND !== 'esplora') {
@ -739,6 +744,52 @@ 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) { private getBlockTipHeight(req: Request, res: Response) {
try { try {
const result = blocks.getCurrentBlockHeight(); const result = blocks.getCurrentBlockHeight();

View File

@ -33,8 +33,8 @@ import AccelerationRepository from '../repositories/AccelerationRepository';
import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp'; import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
import mempool from './mempool'; import mempool from './mempool';
import CpfpRepository from '../repositories/CpfpRepository'; import CpfpRepository from '../repositories/CpfpRepository';
import accelerationApi from './services/acceleration';
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script'; import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
import database from '../database';
class Blocks { class Blocks {
private blocks: BlockExtended[] = []; private blocks: BlockExtended[] = [];
@ -1462,6 +1462,36 @@ class Blocks {
// not a fatal error, we'll try again next time the indexer runs // 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(); export default new Blocks();

View File

@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2'; import { RowDataPacket } from 'mysql2';
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 94; private static currentVersion = 95;
private queryTimeout = 3600_000; private queryTimeout = 3600_000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; private uniqueLogs: string[] = [];
@ -1118,6 +1118,18 @@ class DatabaseMigration {
} }
await this.updateToSchemaVersion(94); 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);
}
} }
/** /**

View File

@ -19,15 +19,6 @@ class PoolsParser {
'addresses': '[]', 'addresses': '[]',
'slug': 'unknown' '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 { public setMiningPools(pools): void {
for (const pool of pools) { for (const pool of pools) {

View File

@ -0,0 +1,105 @@
import { WebSocket } from 'ws';
import logger from '../../logger';
import config from '../../config';
import websocketHandler from '../websocket-handler';
export interface StratumJob {
pool: number;
height: number;
coinbase: string;
scriptsig: string;
reward: number;
jobId: string;
extraNonce: string;
extraNonce2Size: number;
prevHash: string;
coinbase1: string;
coinbase2: string;
merkleBranches: string[];
version: string;
bits: string;
time: string;
timestamp: number;
cleanJobs: boolean;
received: number;
}
function isStratumJob(obj: any): obj is StratumJob {
return obj
&& typeof obj === 'object'
&& 'pool' in obj
&& 'prevHash' in obj
&& 'height' in obj
&& 'received' in obj
&& 'version' in obj
&& 'timestamp' in obj
&& 'bits' in obj
&& 'merkleBranches' in obj
&& 'cleanJobs' in obj;
}
class StratumApi {
private ws: WebSocket | null = null;
private runWebsocketLoop: boolean = false;
private startedWebsocketLoop: boolean = false;
private websocketConnected: boolean = false;
private jobs: Record<string, StratumJob> = {};
public constructor() {}
public getJobs(): Record<string, StratumJob> {
return this.jobs;
}
private handleWebsocketMessage(msg: any): void {
if (isStratumJob(msg)) {
this.jobs[msg.pool] = msg;
websocketHandler.handleNewStratumJob(this.jobs[msg.pool]);
}
}
public async connectWebsocket(): Promise<void> {
if (!config.STRATUM.ENABLED) {
return;
}
this.runWebsocketLoop = true;
if (this.startedWebsocketLoop) {
return;
}
while (this.runWebsocketLoop) {
this.startedWebsocketLoop = true;
if (!this.ws) {
this.ws = new WebSocket(`${config.STRATUM.API}`);
this.websocketConnected = true;
this.ws.on('open', () => {
logger.info('Stratum websocket opened');
});
this.ws.on('error', (error) => {
logger.err('Stratum websocket error: ' + error);
this.ws = null;
this.websocketConnected = false;
});
this.ws.on('close', () => {
logger.info('Stratum websocket closed');
this.ws = null;
this.websocketConnected = false;
});
this.ws.on('message', (data, isBinary) => {
try {
const parsedMsg = JSON.parse((isBinary ? data : data.toString()) as string);
this.handleWebsocketMessage(parsedMsg);
} catch (e) {
logger.warn('Failed to parse stratum websocket message: ' + (e instanceof Error ? e.message : e));
}
});
}
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
}
export default new StratumApi();

View File

@ -38,6 +38,7 @@ interface AddressTransactions {
import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import { calculateMempoolTxCpfp } from './cpfp'; import { calculateMempoolTxCpfp } from './cpfp';
import { getRecentFirstSeen } from '../utils/file-read'; import { getRecentFirstSeen } from '../utils/file-read';
import stratumApi, { StratumJob } from './services/stratum';
// valid 'want' subscriptions // valid 'want' subscriptions
const wantable = [ const wantable = [
@ -403,6 +404,16 @@ class WebsocketHandler {
delete client['track-mempool']; delete client['track-mempool'];
} }
if (parsedMessage && parsedMessage['track-stratum'] != null) {
if (parsedMessage['track-stratum']) {
const sub = parsedMessage['track-stratum'];
client['track-stratum'] = sub;
response['stratumJobs'] = this.socketData['stratumJobs'];
} else {
client['track-stratum'] = false;
}
}
if (Object.keys(response).length) { if (Object.keys(response).length) {
client.send(this.serializeResponse(response)); client.send(this.serializeResponse(response));
} }
@ -1384,6 +1395,23 @@ class WebsocketHandler {
await statistics.runStatistics(); await statistics.runStatistics();
} }
public handleNewStratumJob(job: StratumJob): void {
this.updateSocketDataFields({ 'stratumJobs': stratumApi.getJobs() });
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
if (client['track-stratum'] && (client['track-stratum'] === 'all' || client['track-stratum'] === job.pool)) {
client.send(JSON.stringify({
'stratumJob': job
}));
}
});
}
}
// takes a dictionary of JSON serialized values // takes a dictionary of JSON serialized values
// and zips it together into a valid JSON object // and zips it together into a valid JSON object
private serializeResponse(response): string { private serializeResponse(response): string {

View File

@ -165,6 +165,10 @@ interface IConfig {
WALLETS: { WALLETS: {
ENABLED: boolean; ENABLED: boolean;
WALLETS: string[]; WALLETS: string[];
},
STRATUM: {
ENABLED: boolean;
API: string;
} }
} }
@ -332,6 +336,10 @@ const defaults: IConfig = {
'ENABLED': false, 'ENABLED': false,
'WALLETS': [], 'WALLETS': [],
}, },
'STRATUM': {
'ENABLED': false,
'API': 'http://localhost:1234',
}
}; };
class Config implements IConfig { class Config implements IConfig {
@ -354,6 +362,7 @@ class Config implements IConfig {
REDIS: IConfig['REDIS']; REDIS: IConfig['REDIS'];
FIAT_PRICE: IConfig['FIAT_PRICE']; FIAT_PRICE: IConfig['FIAT_PRICE'];
WALLETS: IConfig['WALLETS']; WALLETS: IConfig['WALLETS'];
STRATUM: IConfig['STRATUM'];
constructor() { constructor() {
const configs = this.merge(configFromFile, defaults); const configs = this.merge(configFromFile, defaults);
@ -376,6 +385,7 @@ class Config implements IConfig {
this.REDIS = configs.REDIS; this.REDIS = configs.REDIS;
this.FIAT_PRICE = configs.FIAT_PRICE; this.FIAT_PRICE = configs.FIAT_PRICE;
this.WALLETS = configs.WALLETS; this.WALLETS = configs.WALLETS;
this.STRATUM = configs.STRATUM;
} }
merge = (...objects: object[]): IConfig => { merge = (...objects: object[]): IConfig => {

View File

@ -48,6 +48,7 @@ import accelerationRoutes from './api/acceleration/acceleration.routes';
import aboutRoutes from './api/about.routes'; import aboutRoutes from './api/about.routes';
import mempoolBlocks from './api/mempool-blocks'; import mempoolBlocks from './api/mempool-blocks';
import walletApi from './api/services/wallets'; import walletApi from './api/services/wallets';
import stratumApi from './api/services/stratum';
class Server { class Server {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
@ -320,6 +321,9 @@ class Server {
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler)); loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
accelerationApi.connectWebsocket(); accelerationApi.connectWebsocket();
if (config.STRATUM.ENABLED) {
stratumApi.connectWebsocket();
}
} }
setUpHttpApiRoutes(): void { setUpHttpApiRoutes(): void {

View File

@ -325,6 +325,8 @@ export interface BlockExtension {
// Requires coinstatsindex, will be set to NULL otherwise // Requires coinstatsindex, will be set to NULL otherwise
utxoSetSize: number | null; utxoSetSize: number | null;
totalInputAmt: number | null; totalInputAmt: number | null;
// pools-v2.json git hash
definitionHash: string | undefined;
} }
/** /**

View File

@ -15,6 +15,7 @@ import blocks from '../api/blocks';
import BlocksAuditsRepository from './BlocksAuditsRepository'; import BlocksAuditsRepository from './BlocksAuditsRepository';
import transactionUtils from '../api/transaction-utils'; import transactionUtils from '../api/transaction-utils';
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script'; import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
import poolsUpdater from '../tasks/pools-updater';
interface DatabaseBlock { interface DatabaseBlock {
id: string; id: string;
@ -114,16 +115,16 @@ class BlocksRepository {
try { try {
const query = `INSERT INTO blocks( const query = `INSERT INTO blocks(
height, hash, blockTimestamp, size, height, hash, blockTimestamp, size,
weight, tx_count, coinbase_raw, difficulty, weight, tx_count, coinbase_raw, difficulty,
pool_id, fees, fee_span, median_fee, pool_id, fees, fee_span, median_fee,
reward, version, bits, nonce, reward, version, bits, nonce,
merkle_root, previous_block_hash, avg_fee, avg_fee_rate, merkle_root, previous_block_hash, avg_fee, avg_fee_rate,
median_timestamp, header, coinbase_address, coinbase_addresses, median_timestamp, header, coinbase_address, coinbase_addresses,
coinbase_signature, utxoset_size, utxoset_change, avg_tx_size, coinbase_signature, utxoset_size, utxoset_change, avg_tx_size,
total_inputs, total_outputs, total_input_amt, total_output_amt, total_inputs, total_outputs, total_input_amt, total_output_amt,
fee_percentiles, segwit_total_txs, segwit_total_size, segwit_total_weight, fee_percentiles, segwit_total_txs, segwit_total_size, segwit_total_weight,
median_fee_amt, coinbase_signature_ascii median_fee_amt, coinbase_signature_ascii, definition_hash
) VALUE ( ) VALUE (
?, ?, FROM_UNIXTIME(?), ?, ?, ?, FROM_UNIXTIME(?), ?,
?, ?, ?, ?, ?, ?, ?, ?,
@ -134,7 +135,7 @@ class BlocksRepository {
?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?,
?, ? ?, ?, ?
)`; )`;
const poolDbId = await PoolsRepository.$getPoolByUniqueId(block.extras.pool.id); const poolDbId = await PoolsRepository.$getPoolByUniqueId(block.extras.pool.id);
@ -181,6 +182,7 @@ class BlocksRepository {
block.extras.segwitTotalWeight, block.extras.segwitTotalWeight,
block.extras.medianFeeAmt, block.extras.medianFeeAmt,
truncatedCoinbaseSignatureAscii, truncatedCoinbaseSignatureAscii,
poolsUpdater.currentSha
]; ];
await DB.query(query, params); await DB.query(query, params);
@ -1013,9 +1015,9 @@ class BlocksRepository {
public async $savePool(id: string, poolId: number): Promise<void> { public async $savePool(id: string, poolId: number): Promise<void> {
try { try {
await DB.query(` await DB.query(`
UPDATE blocks SET pool_id = ? UPDATE blocks SET pool_id = ?, definition_hash = ?
WHERE hash = ?`, WHERE hash = ?`,
[poolId, id] [poolId, poolsUpdater.currentSha, id]
); );
} catch (e) { } catch (e) {
logger.err(`Cannot update block pool. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot update block pool. Reason: ` + (e instanceof Error ? e.message : e));

View File

@ -88,8 +88,8 @@ class PoolsUpdater {
try { try {
await DB.query('START TRANSACTION;'); await DB.query('START TRANSACTION;');
await poolsParser.migratePoolsJson();
await this.updateDBSha(githubSha); await this.updateDBSha(githubSha);
await poolsParser.migratePoolsJson();
await DB.query('COMMIT;'); await DB.query('COMMIT;');
} catch (e) { } catch (e) {
logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, this.tag); 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 * Fetch our latest pools-v2.json sha from the db
*/ */
private async getShaFromDb(): Promise<string | null> { public async getShaFromDb(): Promise<string | null> {
try { try {
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"'); const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
return (rows.length > 0 ? rows[0].string : null); return (rows.length > 0 ? rows[0].string : null);

View File

@ -148,6 +148,10 @@
"API": "__MEMPOOL_SERVICES_API__", "API": "__MEMPOOL_SERVICES_API__",
"ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__ "ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
}, },
"STRATUM": {
"ENABLED": __STRATUM_ENABLED__,
"API": "__STRATUM_API__"
},
"REDIS": { "REDIS": {
"ENABLED": __REDIS_ENABLED__, "ENABLED": __REDIS_ENABLED__,
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__", "UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__",

View File

@ -149,6 +149,10 @@ __REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"} __MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"}
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false} __MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
# STRATUM
__STRATUM_ENABLED__=${STRATUM_ENABLED:=false}
__STRATUM_API__=${STRATUM_API:="http://localhost:1234"}
# REDIS # REDIS
__REDIS_ENABLED__=${REDIS_ENABLED:=false} __REDIS_ENABLED__=${REDIS_ENABLED:=false}
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""} __REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""}
@ -300,6 +304,10 @@ sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.j
sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json
sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json
# STRATUM
sed -i "s!__STRATUM_ENABLED__!${__STRATUM_ENABLED__}!g" mempool-config.json
sed -i "s!__STRATUM_API__!${__STRATUM_API__}!g" mempool-config.json
# REDIS # REDIS
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json

View File

@ -11,10 +11,14 @@ services:
stop_grace_period: 1m stop_grace_period: 1m
command: "./wait-for db:3306 --timeout=720 -- nginx -g 'daemon off;'" command: "./wait-for db:3306 --timeout=720 -- nginx -g 'daemon off;'"
ports: ports:
- 80:8080 - 8080:8080
api: api:
environment: environment:
MEMPOOL_BACKEND: "none" MEMPOOL_BACKEND: "electrum"
ELECTRUM_HOST: "172.27.0.1"
ELECTRUM_PORT: "50001"
ELECTRUM_TLS_ENABLED: "false"
CORE_RPC_HOST: "172.27.0.1" CORE_RPC_HOST: "172.27.0.1"
CORE_RPC_PORT: "8332" CORE_RPC_PORT: "8332"
CORE_RPC_USERNAME: "mempool" CORE_RPC_USERNAME: "mempool"

View File

@ -45,6 +45,7 @@ __SERVICES_API__=${SERVICES_API:=https://mempool.space/api/v1/services}
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false} __PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true} __HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false} __ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
__STRATUM_ENABLED__=${STRATUM_ENABLED:=false}
# Export as environment variables to be used by envsubst # Export as environment variables to be used by envsubst
export __MAINNET_ENABLED__ export __MAINNET_ENABLED__
@ -76,6 +77,7 @@ export __SERVICES_API__
export __PUBLIC_ACCELERATIONS__ export __PUBLIC_ACCELERATIONS__
export __HISTORICAL_PRICE__ export __HISTORICAL_PRICE__
export __ADDITIONAL_CURRENCIES__ export __ADDITIONAL_CURRENCIES__
export __STRATUM_ENABLED__
folder=$(find /var/www/mempool -name "config.js" | xargs dirname) folder=$(find /var/www/mempool -name "config.js" | xargs dirname)
echo ${folder} echo ${folder}

View File

@ -13,8 +13,8 @@ localhostIP="127.0.0.1"
cp ./docker/frontend/* ./frontend cp ./docker/frontend/* ./frontend
cp ./nginx.conf ./frontend/ cp ./nginx.conf ./frontend/
cp ./nginx-mempool.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}: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}/0.0.0.0/g" ./frontend/nginx.conf
sed -i"" -e "s/user nobody;//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!/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 sed -i"" -e "s/${localhostIP}:8999/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/g" ./frontend/nginx-mempool.conf

View File

@ -344,7 +344,9 @@ describe('Mainnet', () => {
cy.visit('/'); cy.visit('/');
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.changeNetwork('testnet4'); //TODO(knorrium): add a check for the proxied server
// cy.changeNetwork('testnet4');
cy.changeNetwork('signet'); cy.changeNetwork('signet');
cy.changeNetwork('mainnet'); cy.changeNetwork('mainnet');
}); });

View File

@ -27,5 +27,6 @@
"ACCELERATOR": false, "ACCELERATOR": false,
"ACCELERATOR_BUTTON": true, "ACCELERATOR_BUTTON": true,
"PUBLIC_ACCELERATIONS": false, "PUBLIC_ACCELERATIONS": false,
"STRATUM_ENABLED": false,
"SERVICES_API": "https://mempool.space/api/v1/services" "SERVICES_API": "https://mempool.space/api/v1/services"
} }

View File

@ -33,7 +33,7 @@
"browserify": "^17.0.0", "browserify": "^17.0.0",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"domino": "^2.1.6", "domino": "^2.1.6",
"echarts": "~5.5.0", "echarts": "~5.6.0",
"esbuild": "^0.24.0", "esbuild": "^0.24.0",
"ngx-echarts": "~17.2.0", "ngx-echarts": "~17.2.0",
"ngx-infinite-scroll": "^17.0.0", "ngx-infinite-scroll": "^17.0.0",
@ -8724,12 +8724,12 @@
} }
}, },
"node_modules/echarts": { "node_modules/echarts": {
"version": "5.5.0", "version": "5.6.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz", "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
"integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==", "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
"dependencies": { "dependencies": {
"tslib": "2.3.0", "tslib": "2.3.0",
"zrender": "5.5.0" "zrender": "5.6.1"
} }
}, },
"node_modules/echarts/node_modules/tslib": { "node_modules/echarts/node_modules/tslib": {
@ -18366,9 +18366,9 @@
} }
}, },
"node_modules/zrender": { "node_modules/zrender": {
"version": "5.5.0", "version": "5.6.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz", "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
"integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==", "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
"dependencies": { "dependencies": {
"tslib": "2.3.0" "tslib": "2.3.0"
} }
@ -24485,12 +24485,12 @@
} }
}, },
"echarts": { "echarts": {
"version": "5.5.0", "version": "5.6.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz", "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
"integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==", "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
"requires": { "requires": {
"tslib": "2.3.0", "tslib": "2.3.0",
"zrender": "5.5.0" "zrender": "5.6.1"
}, },
"dependencies": { "dependencies": {
"tslib": { "tslib": {
@ -31485,9 +31485,9 @@
} }
}, },
"zrender": { "zrender": {
"version": "5.5.0", "version": "5.6.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz", "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
"integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==", "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
"requires": { "requires": {
"tslib": "2.3.0" "tslib": "2.3.0"
}, },

View File

@ -86,7 +86,7 @@
"browserify": "^17.0.0", "browserify": "^17.0.0",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"domino": "^2.1.6", "domino": "^2.1.6",
"echarts": "~5.5.0", "echarts": "~5.6.0",
"ngx-echarts": "~17.2.0", "ngx-echarts": "~17.2.0",
"ngx-infinite-scroll": "^17.0.0", "ngx-infinite-scroll": "^17.0.0",
"qrcode": "1.5.1", "qrcode": "1.5.1",

View File

@ -3,8 +3,10 @@ const fs = require('fs');
let PROXY_CONFIG = require('./proxy.conf'); let PROXY_CONFIG = require('./proxy.conf');
PROXY_CONFIG.forEach(entry => { PROXY_CONFIG.forEach(entry => {
entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space"); const hostname = process.env.CYPRESS_REROUTE_TESTNET === 'true' ? 'mempool-staging.fra.mempool.space' : 'node201.fmt.mempool.space';
entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space"); console.log(`e2e tests running against ${hostname}`);
entry.target = entry.target.replace("mempool.space", hostname);
entry.target = entry.target.replace("liquid.network", "liquid-staging.fmt.mempool.space");
}); });
module.exports = PROXY_CONFIG; module.exports = PROXY_CONFIG;

View File

@ -217,7 +217,7 @@
<ng-container> <ng-container>
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales"> <ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
<a [href]="'https://x.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username"> <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 + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/> <img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a> </a>
</ng-template> </ng-template>
</ng-container> </ng-container>
@ -229,7 +229,7 @@
<div class="wrapper"> <div class="wrapper">
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads"> <ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
<a [href]="'https://x.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username"> <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 + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/> <img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a> </a>
</ng-template> </ng-template>
</div> </div>

View File

@ -1,10 +1,18 @@
<div class="box card w-100" style="background: var(--box-bg)" id=acceleratePreviewAnchor> <div class="box card w-100 accelerate-checkout-inner" [class.input-disabled]="isCheckoutLocked > 0" style="background: var(--box-bg)" id=acceleratePreviewAnchor>
@if (accelerateError) { @if (accelerateError) {
<div class="row mb-1 text-center"> @if (accelerateError.includes('Payment declined')) {
<div class="col-sm"> <div class="row mb-1 text-center">
<h1 style="font-size: larger;" i18n="accelerator.sorry-error-title">Sorry, something went wrong!</h1> <div class="col-sm">
<h1 style="font-size: larger;">{{ accelerateError }}</h1>
</div>
</div> </div>
</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 class="row text-center mt-1"> <div class="row text-center mt-1">
<div class="col-sm"> <div class="col-sm">
<div class="d-flex flex-row justify-content-center align-items-center"> <div class="d-flex flex-row justify-content-center align-items-center">
@ -357,11 +365,11 @@
<app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true" class="ml-2"></app-active-acceleration-box> <app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true" class="ml-2"></app-active-acceleration-box>
</div> </div>
</div> </div>
<div class="payment-area mt-2 p-2" style="font-size: 14px;"> <div class="payment-area" style="font-size: 14px;">
<div class="row text-center justify-content-center mx-2"> <div class="row text-center justify-content-center mx-2">
<p i18n="accelerator.payment-to-mempool-space">Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank">{{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}</a></p> <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>
</div> </div>
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) { @if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay)) {
<div class="row"> <div class="row">
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center"> <div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
<p><ng-container i18n="accelerator.your-account-will-be-debited">Your account will be debited no more than</ng-container>&nbsp;<small style="font-family: monospace;">{{ cost | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></p> <p><ng-container i18n="accelerator.your-account-will-be-debited">Your account will be debited no more than</ng-container>&nbsp;<small style="font-family: monospace;">{{ cost | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></p>
@ -378,9 +386,12 @@
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container>&nbsp;<span><small style="font-family: monospace;">{{ ((invoice.btcDue * 100_000_000) || cost) | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></span></p> <p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container>&nbsp;<span><small style="font-family: monospace;">{{ ((invoice.btcDue * 100_000_000) || cost) | number }}</small>&nbsp;<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> <app-bitcoin-invoice style="width: 100%;" [invoice]="invoice" [minimal]="true" (completed)="bitcoinPaymentCompleted()"></app-bitcoin-invoice>
} @else if (btcpayInvoiceFailed) { } @else if (btcpayInvoiceFailed) {
<p i18n="accelerator.failed-to-load-invoice">Failed to load invoice</p> <div class="btcpay-invoice">
<div class="d-flex flex-column align-items-center justify-content-center" style="width: 100%; height: 292px;"> <fa-icon style="font-size: 20px; color: var(--red)" [icon]="['fas', 'circle-xmark']"></fa-icon>
<fa-icon style="font-size: 24px; 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>
}
</div> </div>
} @else { } @else {
<p i18n="accelerator.loading-invoice">Loading invoice...</p> <p i18n="accelerator.loading-invoice">Loading invoice...</p>
@ -389,13 +400,13 @@
</div> </div>
} }
</div> </div>
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) { @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {
<div class="col-sm text-center flex-grow-0 d-flex flex-column justify-content-center align-items-center"> <div class="col-sm text-center flex-grow-0 d-flex flex-column justify-content-center align-items-center">
<p class="text-nowrap">&mdash;<span i18n="or">OR</span>&mdash;</p> <p class="text-nowrap">&mdash;&mdash;<span i18n="or"> OR </span>&mdash;&mdash;</p>
</div> </div>
} }
} }
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) { @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center"> <div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container>&nbsp;<app-fiat [value]="cost"></app-fiat> with</p> <p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container>&nbsp;<app-fiat [value]="cost"></app-fiat> with</p>
@if (canPayWithCashapp) { @if (canPayWithCashapp) {
@ -413,6 +424,17 @@
<img src="/resources/google-pay.png" height=37> <img src="/resources/google-pay.png" height=37>
</div> </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>
} }
</div> </div>
@ -435,7 +457,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> <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>
</div> </div>
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay') { } @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay' || step === 'cardonfile') {
<!-- Show checkout page --> <!-- Show checkout page -->
<div class="row mb-md-1 text-center" id="confirm-title"> <div class="row mb-md-1 text-center" id="confirm-title">
<div class="col-sm" id="confirm-payment-title"> <div class="col-sm" id="confirm-payment-title">
@ -451,7 +473,7 @@
</div> </div>
</div> </div>
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay) { @if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay || step === 'cardonfile' && !loadingCardOnFile) {
<div class="row text-center mt-1"> <div class="row text-center mt-1">
<div class="col-sm"> <div class="col-sm">
<div class="form-group w-100"> <div class="form-group w-100">
@ -476,14 +498,24 @@
<div id="cash-app-pay" class="d-inline-block" style="height: 50px" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div> <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') { } @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> <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) { @if (loadingCashapp || loadingApplePay || loadingGooglePay || loadingCardOnFile) {
<div display="d-flex flex-row justify-content-center"> <div display="d-flex flex-row justify-content-center">
<span i18n="accelerator.loading-payment-method">Loading payment method...</span> <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 class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div> </div>
} }
</div> </div>
@if (isTokenizing > 0) {
<div class="d-flex flex-row justify-content-center">
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
}
</div> </div>
</div> </div>

View File

@ -8,6 +8,13 @@
color: var(--green) color: var(--green)
} }
.accelerate-checkout-inner {
&.input-disabled {
pointer-events: none;
opacity: 0.75;
}
}
.paymentMethod { .paymentMethod {
padding: 10px; padding: 10px;
background-color: var(--secondary); background-color: var(--secondary);
@ -146,6 +153,11 @@
.payment-area { .payment-area {
background: var(--bg); background: var(--bg);
margin-top: 0.5rem;
padding: 0.5rem;
@media (max-width: 575px) {
padding-bottom: 1.25rem;
}
} }
.col.pie { .col.pie {
@ -212,4 +224,17 @@
} }
.apple-pay-button-white-with-line { .apple-pay-button-white-with-line {
-apple-pay-button-style: white-outline; -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;
}
} }

View File

@ -13,7 +13,7 @@ import { EnterpriseService } from '@app/services/enterprise.service';
import { ApiService } from '@app/services/api.service'; import { ApiService } from '@app/services/api.service';
import { isDevMode } from '@angular/core'; import { isDevMode } from '@angular/core';
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay'; export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay' | 'cardOnFile';
export type AccelerationEstimate = { export type AccelerationEstimate = {
hasAccess: boolean; hasAccess: boolean;
@ -26,7 +26,7 @@ export type AccelerationEstimate = {
mempoolBaseFee: number; mempoolBaseFee: number;
vsizeFee: number; vsizeFee: number;
pools: number[]; pools: number[];
availablePaymentMethods: Record<PaymentMethod, {min: number, max: number}>; availablePaymentMethods: Record<PaymentMethod, {min: number, max: number, card?: {card_id: string, last_4: string, brand: string, name: string, billing: any}}>;
unavailable?: boolean; unavailable?: boolean;
options: { // recommended bid options options: { // recommended bid options
fee: number; // recommended userBid in sats fee: number; // recommended userBid in sats
@ -49,7 +49,7 @@ export const MIN_BID_RATIO = 1;
export const DEFAULT_BID_RATIO = 2; export const DEFAULT_BID_RATIO = 2;
export const MAX_BID_RATIO = 4; export const MAX_BID_RATIO = 4;
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'processing' | 'paid' | 'success'; type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'cardonfile' | 'processing' | 'paid' | 'success';
@Component({ @Component({
selector: 'app-accelerate-checkout', selector: 'app-accelerate-checkout',
@ -62,9 +62,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
@Input() miningStats: MiningStats; @Input() miningStats: MiningStats;
@Input() eta: ETA; @Input() eta: ETA;
@Input() scrollEvent: boolean; @Input() scrollEvent: boolean;
@Input() cashappEnabled: boolean = true;
@Input() applePayEnabled: boolean = false; @Input() applePayEnabled: boolean = false;
@Input() googlePayEnabled: boolean = true; @Input() googlePayEnabled: boolean = true;
@Input() cardOnFileEnabled: boolean = true;
@Input() advancedEnabled: boolean = false; @Input() advancedEnabled: boolean = false;
@Input() forceMobile: boolean = false; @Input() forceMobile: boolean = false;
@Input() showDetails: boolean = false; @Input() showDetails: boolean = false;
@ -76,6 +76,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
calculating = true; calculating = true;
processing = false; processing = false;
isCheckoutLocked = 0; // reference counter, 0 = unlocked, >0 = locked
isTokenizing = 0; // reference counter, 0 = false, >0 = true
selectedOption: 'wait' | 'accel'; selectedOption: 'wait' | 'accel';
cantPayReason = ''; cantPayReason = '';
quoteError = ''; // error fetching estimate or initial data quoteError = ''; // error fetching estimate or initial data
@ -115,6 +117,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
loadingCashapp = false; loadingCashapp = false;
loadingApplePay = false; loadingApplePay = false;
loadingGooglePay = false; loadingGooglePay = false;
loadingCardOnFile = false;
payments: any; payments: any;
cashAppPay: any; cashAppPay: any;
applePay: any; applePay: any;
@ -154,7 +157,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerateError = null; this.accelerateError = null;
this.timePaid = 0; this.timePaid = 0;
this.btcpayInvoiceFailed = false; this.btcpayInvoiceFailed = false;
this.moveToStep('summary'); this.moveToStep('summary', true);
} else { } else {
this.auth = auth; this.auth = auth;
} }
@ -163,11 +166,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('cash_request_id')) { // Redirected from cashapp if (urlParams.get('cash_request_id')) { // Redirected from cashapp
this.moveToStep('processing'); this.moveToStep('processing', true);
this.insertSquare(); this.insertSquare();
this.setupSquare(); this.setupSquare();
} else { } else {
this.moveToStep('summary'); this.moveToStep('summary', true);
} }
this.conversionsSubscription = this.stateService.conversions$.subscribe( this.conversionsSubscription = this.stateService.conversions$.subscribe(
@ -192,20 +195,23 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
if (changes.accelerating && this.accelerating) { if (changes.accelerating && this.accelerating) {
if (this.step === 'processing' || this.step === 'paid') { if (this.step === 'processing' || this.step === 'paid') {
this.moveToStep('success'); this.moveToStep('success', true);
} else { // Edge case where the transaction gets accelerated by someone else or on another session } else { // Edge case where the transaction gets accelerated by someone else or on another session
this.closeModal(); this.closeModal();
} }
} }
} }
moveToStep(step: CheckoutStep): void { moveToStep(step: CheckoutStep, force: boolean = false): void {
if (this.isCheckoutLocked > 0 && !force) {
return;
}
this.processing = false; this.processing = false;
this._step = step; this._step = step;
if (this.timeoutTimer) { if (this.timeoutTimer) {
clearTimeout(this.timeoutTimer); clearTimeout(this.timeoutTimer);
} }
if (!this.estimate && ['quote', 'summary', 'checkout'].includes(this.step)) { if (!this.estimate && ['quote', 'summary', 'checkout', 'processing'].includes(this.step)) {
this.fetchEstimate(); this.fetchEstimate();
} }
if (this._step === 'checkout') { if (this._step === 'checkout') {
@ -214,10 +220,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
if (this._step === 'checkout' && this.canPayWithBitcoin) { if (this._step === 'checkout' && this.canPayWithBitcoin) {
this.btcpayInvoiceFailed = false; this.btcpayInvoiceFailed = false;
this.loadingBtcpayInvoice = true;
this.invoice = null; this.invoice = null;
this.requestBTCPayInvoice(); this.requestBTCPayInvoice();
} else if (this._step === 'cashapp' && this.cashappEnabled) { } else if (this._step === 'cashapp') {
this.loadingCashapp = true; this.loadingCashapp = true;
this.setupSquare(); this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100); this.scrollToElementWithTimeout('confirm-title', 'center', 100);
@ -229,6 +234,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.loadingGooglePay = true; this.loadingGooglePay = true;
this.setupSquare(); this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100); 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') { } else if (this._step === 'paid') {
this.timePaid = Date.now(); this.timePaid = Date.now();
this.timeoutTimer = setTimeout(() => { this.timeoutTimer = setTimeout(() => {
@ -242,7 +251,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
closeModal(): void { closeModal(): void {
this.completed.emit(true); this.completed.emit(true);
this.moveToStep('summary'); this.moveToStep('summary', true);
} }
/** /**
@ -323,7 +332,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
if (this.step === 'checkout' && this.canPayWithBitcoin && !this.loadingBtcpayInvoice) { if (this.step === 'checkout' && this.canPayWithBitcoin && !this.loadingBtcpayInvoice) {
this.loadingBtcpayInvoice = true;
this.requestBTCPayInvoice(); this.requestBTCPayInvoice();
} }
@ -393,7 +401,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.audioService.playSound('ascend-chime-cartoon'); this.audioService.playSound('ascend-chime-cartoon');
this.showSuccess = true; this.showSuccess = true;
this.estimateSubscription.unsubscribe(); this.estimateSubscription.unsubscribe();
this.moveToStep('paid'); this.moveToStep('paid', true);
}, },
error: (response) => { error: (response) => {
this.processing = false; this.processing = false;
@ -449,6 +457,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
await this.requestApplePayPayment(); await this.requestApplePayPayment();
} else if (this._step === 'googlepay') { } else if (this._step === 'googlepay') {
await this.requestGooglePayPayment(); await this.requestGooglePayPayment();
} else if (this._step === 'cardonfile') {
this.loadingCardOnFile = false;
} }
}, },
error: () => { error: () => {
@ -503,56 +513,75 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
this.loadingApplePay = false; this.loadingApplePay = false;
applePayButton.addEventListener('click', async event => { applePayButton.addEventListener('click', async event => {
if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
return;
}
event.preventDefault(); event.preventDefault();
const tokenResult = await this.applePay.tokenize(); try {
if (tokenResult?.status === 'OK') { // lock the checkout UI and show a loading spinner until the square modals are finished
const card = tokenResult.details?.card; this.isCheckoutLocked++;
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) { this.isTokenizing++;
console.error(`Cannot retreive payment card details`); const tokenResult = await this.applePay.tokenize();
this.accelerateError = 'apple_pay_no_card_details'; if (tokenResult?.status === 'OK') {
this.processing = false; const card = tokenResult.details?.card;
return; if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
} console.error(`Cannot retreive payment card details`);
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase()); this.accelerateError = 'apple_pay_no_card_details';
this.servicesApiService.accelerateWithApplePay$(
this.tx.txid,
tokenResult.token,
cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
costUSD
).subscribe({
next: () => {
this.processing = false; this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); return;
this.audioService.playSound('ascend-chime-cartoon');
if (this.applePay) {
this.applePay.destroy();
}
setTimeout(() => {
this.moveToStep('paid');
}, 1000);
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
// Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 3000);
}
} }
}); const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
} else { // keep checkout in loading state until the acceleration request completes
this.processing = false; this.isTokenizing++;
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`; this.isCheckoutLocked++;
if (tokenResult.errors) { this.servicesApiService.accelerateWithApplePay$(
errorMessage += ` and errors: ${JSON.stringify( this.tx.txid,
tokenResult.errors, tokenResult.token,
)}`; cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
costUSD
).subscribe({
next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
if (this.applePay) {
this.applePay.destroy();
}
setTimeout(() => {
this.isTokenizing--;
this.isCheckoutLocked--;
this.moveToStep('paid', true);
}, 1000);
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
this.isTokenizing--;
this.isCheckoutLocked--;
// Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 10000);
}
}
});
} else {
this.processing = false;
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify(
tokenResult.errors,
)}`;
}
throw new Error(errorMessage);
} }
throw new Error(errorMessage); } finally {
// always unlock the checkout once we're finished
this.isTokenizing--;
this.isCheckoutLocked--;
} }
}); });
} catch (e) { } catch (e) {
@ -602,71 +631,193 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.loadingGooglePay = false; this.loadingGooglePay = false;
document.getElementById('google-pay-button').addEventListener('click', async event => { document.getElementById('google-pay-button').addEventListener('click', async event => {
if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
return;
}
event.preventDefault(); event.preventDefault();
const tokenResult = await this.googlePay.tokenize(); try {
if (tokenResult?.status === 'OK') { // lock the checkout UI and show a loading spinner until the square modals are finished
const card = tokenResult.details?.card; this.isCheckoutLocked++;
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) { this.isTokenizing++;
console.error(`Cannot retreive payment card details`); const tokenResult = await this.googlePay.tokenize();
this.accelerateError = 'apple_pay_no_card_details'; if (tokenResult?.status === 'OK') {
this.processing = false; const card = tokenResult.details?.card;
return; if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
} console.error(`Cannot retreive payment card details`);
const verificationToken = await this.$verifyBuyer(this.payments, tokenResult.token, tokenResult.details, costUSD.toFixed(2)); this.accelerateError = 'apple_pay_no_card_details';
if (!verificationToken || !verificationToken.token) {
console.error(`SCA verification failed`);
this.accelerateError = 'SCA Verification Failed. Payment Declined.';
this.processing = false;
return;
}
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
this.servicesApiService.accelerateWithGooglePay$(
this.tx.txid,
tokenResult.token,
verificationToken.token,
cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
costUSD,
verificationToken.userChallenged
).subscribe({
next: () => {
this.processing = false; this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); return;
this.audioService.playSound('ascend-chime-cartoon');
if (this.googlePay) {
this.googlePay.destroy();
}
setTimeout(() => {
this.moveToStep('paid');
}, 1000);
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
// Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 3000);
}
} }
}); const verificationToken = await this.$verifyBuyer(this.payments, tokenResult.token, tokenResult.details, costUSD.toFixed(2));
} else { if (!verificationToken || !verificationToken.token) {
this.processing = false; console.error(`SCA verification failed`);
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`; this.accelerateError = 'SCA Verification Failed. Payment Declined.';
if (tokenResult.errors) { this.processing = false;
errorMessage += ` and errors: ${JSON.stringify( return;
tokenResult.errors, }
)}`; const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
// keep checkout in loading state until the acceleration request completes
this.isCheckoutLocked++;
this.isTokenizing++;
this.servicesApiService.accelerateWithGooglePay$(
this.tx.txid,
tokenResult.token,
verificationToken.token,
cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
costUSD,
verificationToken.userChallenged
).subscribe({
next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
if (this.googlePay) {
this.googlePay.destroy();
}
setTimeout(() => {
this.isTokenizing--;
this.isCheckoutLocked--;
this.moveToStep('paid', true);
}, 1000);
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
this.isTokenizing--;
this.isCheckoutLocked--;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
// Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 10000);
}
}
});
} else {
this.processing = false;
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify(
tokenResult.errors,
)}`;
}
throw new Error(errorMessage);
} }
throw new Error(errorMessage); } finally {
// always unlock the checkout once we're finished
this.isTokenizing--;
this.isCheckoutLocked--;
} }
}); });
} }
); );
} }
/**
* 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 * CASHAPP
*/ */
@ -687,7 +838,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
const redirectHostname = document.location.hostname === 'localhost' ? `http://localhost:4200`: `https://${document.location.hostname}`; const redirectHostname = document.location.hostname === 'localhost' ? `http://localhost:4200`: `https://${document.location.hostname}`;
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 costUSD = this.cost / 100_000_000 * conversions.USD;
const paymentRequest = this.payments.paymentRequest({ const paymentRequest = this.payments.paymentRequest({
countryCode: 'US', countryCode: 'US',
currencyCode: 'USD', currencyCode: 'USD',
@ -727,7 +878,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.cashAppPay.destroy(); this.cashAppPay.destroy();
} }
setTimeout(() => { setTimeout(() => {
this.moveToStep('paid'); this.moveToStep('paid', true);
if (window.history.replaceState) { if (window.history.replaceState) {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, '')); window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ''));
@ -742,7 +893,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
// Reset everything by reloading the page :D, can be improved // Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``)); window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 3000); }, 10000);
} }
} }
}); });
@ -782,16 +933,19 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* BTCPay * BTCPay
*/ */
async requestBTCPayInvoice(): Promise<void> { async requestBTCPayInvoice(): Promise<void> {
this.loadingBtcpayInvoice = true;
this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe( this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe(
switchMap(response => { switchMap(response => {
return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId); return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId);
}), }),
catchError(error => { catchError(error => {
console.log(error); console.log(error);
this.loadingBtcpayInvoice = false;
this.btcpayInvoiceFailed = true; this.btcpayInvoiceFailed = true;
return of(null); return of(null);
}) })
).subscribe((invoice) => { ).subscribe((invoice) => {
this.loadingBtcpayInvoice = false;
this.invoice = invoice; this.invoice = invoice;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
@ -801,7 +955,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon'); this.audioService.playSound('ascend-chime-cartoon');
this.estimateSubscription.unsubscribe(); this.estimateSubscription.unsubscribe();
this.moveToStep('paid'); this.moveToStep('paid', true);
} }
isLoggedIn(): boolean { isLoggedIn(): boolean {
@ -828,9 +982,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
get couldPayWithCashapp(): boolean { get couldPayWithCashapp(): boolean {
if (!this.cashappEnabled) {
return false;
}
return !!this.estimate?.availablePaymentMethods?.cashapp; return !!this.estimate?.availablePaymentMethods?.cashapp;
} }
@ -865,7 +1016,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
get canPayWithCashapp(): boolean { get canPayWithCashapp(): boolean {
if (!this.cashappEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) { if (!this.conversions || (!this.isProdDomain && !isDevMode())) {
return false; return false;
} }
@ -912,6 +1063,22 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
return false; 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 { get canPayWithBalance(): boolean {
if (!this.hasAccessToBalanceMode) { if (!this.hasAccessToBalanceMode) {
return false; return false;

View File

@ -14,7 +14,7 @@
<th class="time text-right" i18n="accelerator.requested">Requested</th> <th class="time text-right" i18n="accelerator.requested">Requested</th>
</ng-container> </ng-container>
<ng-container *ngIf="!pending"> <ng-container *ngIf="!pending">
<th class="fee text-right" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th> <th class="fee text-right text-truncate" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th>
<th class="block text-right" i18n="shared.block-title">Block</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="pool text-right" i18n="mining.pool-name" *ngIf="!this.widget">Pool</th>
<th class="status text-right" i18n="transaction.status|Transaction Status">Status</th> <th class="status text-right" i18n="transaction.status|Transaction Status">Status</th>
@ -64,7 +64,8 @@
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span> <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'">&nbsp;</span></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'">&nbsp;</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'">&nbsp;</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'">&nbsp;</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'">&nbsp;</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'">&nbsp;</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'">&nbsp;</span></span>
</td> </td>
<td class="date text-right" *ngIf="!this.widget"> <td class="date text-right" *ngIf="!this.widget">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time> <app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>

View File

@ -10,6 +10,10 @@
</span> </span>
} }
<div class="d-flex justify-content-center">
<app-mempool-error *ngIf="paymentErrorMessage" [error]="paymentErrorMessage"></app-mempool-error>
</div>
<div *ngIf="paymentStatus === 2"> <div *ngIf="paymentStatus === 2">
<form [formGroup]="paymentForm"> <form [formGroup]="paymentForm">

View File

@ -1,9 +1,8 @@
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'; import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router'; import { Subscription, of, catchError } from 'rxjs';
import { Subscription, of, timer } from 'rxjs'; import { retry, tap } from 'rxjs/operators';
import { filter, repeat, retry, switchMap, take, tap } from 'rxjs/operators';
import { ServicesApiServices } from '@app/services/services-api.service'; import { ServicesApiServices } from '@app/services/services-api.service';
@Component({ @Component({
@ -18,30 +17,17 @@ export class BitcoinInvoiceComponent implements OnInit, OnChanges, OnDestroy {
@Output() completed = new EventEmitter(); @Output() completed = new EventEmitter();
paymentForm: FormGroup; paymentForm: FormGroup;
requestSubscription: Subscription | undefined;
paymentStatusSubscription: Subscription | undefined; paymentStatusSubscription: Subscription | undefined;
paymentStatus = 1; // 1 - Waiting for invoice | 2 - Pending payment | 3 - Payment completed paymentStatus = 1; // 1 - Waiting for invoice | 2 - Pending payment | 3 - Payment completed
paramMapSubscription: Subscription | undefined; paymentErrorMessage: string = '';
invoiceSubscription: Subscription | undefined;
invoiceTimeout; // Wait for angular to load all the things before making a request
constructor( constructor(
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private apiService: ServicesApiServices, private apiService: ServicesApiServices,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer
private activatedRoute: ActivatedRoute
) { } ) { }
ngOnDestroy() { ngOnDestroy() {
if (this.requestSubscription) {
this.requestSubscription.unsubscribe();
}
if (this.paramMapSubscription) {
this.paramMapSubscription.unsubscribe();
}
if (this.invoiceSubscription) {
this.invoiceSubscription.unsubscribe();
}
if (this.paymentStatusSubscription) { if (this.paymentStatusSubscription) {
this.paymentStatusSubscription.unsubscribe(); this.paymentStatusSubscription.unsubscribe();
} }
@ -72,15 +58,39 @@ export class BitcoinInvoiceComponent implements OnInit, OnChanges, OnDestroy {
} else { } else {
this.paymentStatus = 4; 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( this.paymentStatusSubscription = this.apiService.getPaymentStatus$(this.invoice.btcpayInvoiceId).pipe(
retry({ delay: () => timer(2000)}), tap(result => {
repeat({delay: 2000}), if (result.status === 204) { // Manually trigger an error in that case so we can retry
filter((response) => response.status !== 204 && response.status !== 404), throw result;
take(1), } else if (result.status === 200) { // Invoice settled
).subscribe(() => { this.paymentStatus = 3;
this.paymentStatus = 3; this.completed.emit();
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();
} }
get availableMethods(): string[] { get availableMethods(): string[] {

View File

@ -19,12 +19,10 @@
} @else if (!user) { } @else if (!user) {
<!-- User not logged in --> <!-- User not logged in -->
<div class="alert alert-mempool d-block text-center w-100"> <div class="alert alert-mempool d-block text-center w-100">
<div class="d-inline align-middle"> <div class="d-inline align-middle pr-2">
<span>To use the faucet, please&nbsp;</span> <span>To use the faucet, please</span>
<a routerLink="/login" [queryParams]="{'redirectTo': '/testnet4/faucet'}">login</a>
<span class="mr-2">&nbsp;or</span>
</div> </div>
<app-twitter-login customClass="btn btn-sm" width="220px" redirectTo="/testnet4/faucet" buttonString="Sign up with Twitter"></app-twitter-login> <app-github-login customClass="btn btn-sm" width="150px" redirectTo="/testnet4/faucet" buttonString="Sign up with"></app-github-login>
</div> </div>
} }
@else if (user && user.status === 'pending' && !user.email && user.snsId) { @else if (user && user.status === 'pending' && !user.email && user.snsId) {
@ -36,18 +34,18 @@
</div> </div>
} }
@else if (error === 'not_available') { @else if (error === 'not_available') {
<!-- User logged in but not a paid user or did not link its Twitter account --> <!-- User logged in but not a paid user or did not link its Github account -->
<div class="alert alert-mempool d-block text-center w-100"> <div class="alert alert-mempool d-block text-center w-100">
<div class="d-inline align-middle"> <div class="d-inline align-middle">
<span class="mb-2 mr-2">To use the faucet, please</span> <span class="mb-2 mr-2">To use the faucet, please</span>
</div> </div>
<app-twitter-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your Twitter"></app-twitter-login> <app-github-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your"></app-github-login>
</div> </div>
} }
@else if (error === 'account_limited') { @else if (error === 'account_limited') {
<div class="alert alert-mempool d-block text-center w-100"> <div class="alert alert-mempool d-block text-center w-100">
<div class="d-inline align-middle"> <div class="d-inline align-middle">
<span class="mb-2 mr-2">Your Twitter account does not allow you to access the faucet</span> <span class="mb-2 mr-2">Your account does not allow you to access the faucet</span>
</div> </div>
</div> </div>
} }

View File

@ -24,7 +24,7 @@ export class FaucetComponent implements OnInit, OnDestroy {
min: number; // minimum amount to request at once (in sats) min: number; // minimum amount to request at once (in sats)
max: number; // maximum amount to request at once max: number; // maximum amount to request at once
address?: string; // faucet address address?: string; // faucet address
code: 'ok' | 'faucet_not_available' | 'faucet_maximum_reached' | 'faucet_too_soon'; code: 'ok' | 'faucet_not_available' | 'faucet_maximum_reached' | 'faucet_too_soon' | 'faucet_not_available_no_utxo';
} | null = null; } | null = null;
faucetForm: FormGroup; faucetForm: FormGroup;

View File

@ -0,0 +1,6 @@
<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>

View File

@ -0,0 +1,25 @@
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;
}
}

View File

@ -4,9 +4,8 @@
<nav class="navbar navbar-expand-md navbar-dark"> <nav class="navbar navbar-expand-md navbar-dark">
<!-- Hamburger --> <!-- Hamburger -->
<ng-container *ngIf="servicesEnabled"> <ng-container *ngIf="servicesEnabled">
<div *ngIf="user" class="profile_image_container" [class]="{'anon': !user.imageMd5}" (click)="hamburgerClick($event)"> <div *ngIf="user" class="profile_image_container" (click)="hamburgerClick($event)">
<img *ngIf="user.imageMd5" [src]="'/api/v1/services/account/images/' + user.username + '/md5=' + user.imageMd5" class="profile_image"> <img [src]="'/api/v1/services/account/images/' + user.username" class="profile_image" onError="this.src = '/resources/anon.svg'; this.className = 'anon'" />
<app-svg-images style="color: lightgrey; fill: lightgray" *ngIf="!user.imageMd5" name="anon"></app-svg-images>
</div> </div>
<div *ngIf="false && user === null" class="profile_image_container" (click)="hamburgerClick($event)"> <div *ngIf="false && user === null" class="profile_image_container" (click)="hamburgerClick($event)">
<app-svg-images name="hamburger" height="40"></app-svg-images> <app-svg-images name="hamburger" height="40"></app-svg-images>
@ -23,7 +22,7 @@
} @else { } @else {
<ng-template [ngIf]="subdomain && enterpriseInfo"> <ng-template [ngIf]="subdomain && enterpriseInfo">
<div class="subdomain_container"> <div class="subdomain_container">
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}"> <img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
</div> </div>
<div class="vertical-line"></div> <div class="vertical-line"></div>
</ng-template> </ng-template>
@ -43,7 +42,7 @@
} @else { } @else {
<ng-template [ngIf]="subdomain && enterpriseInfo"> <ng-template [ngIf]="subdomain && enterpriseInfo">
<div class="subdomain_container"> <div class="subdomain_container">
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}"> <img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
</div> </div>
<div class="vertical-line"></div> <div class="vertical-line"></div>
</ng-template> </ng-template>

View File

@ -269,7 +269,7 @@ nav {
text-align: center; text-align: center;
align-self: center; align-self: center;
cursor: pointer; cursor: pointer;
&.anon { .anon {
border: 1.5px solid lightgrey; border: 1.5px solid lightgrey;
color: lightgrey; color: lightgrey;
border-radius: 5px; border-radius: 5px;

View File

@ -10,7 +10,7 @@
<h1 class="m-0 pt-1 pt-md-0">{{ poolStats.pool.name }}</h1> <h1 class="m-0 pt-1 pt-md-0">{{ poolStats.pool.name }}</h1>
</div> </div>
<div class="box"> <div class="box pool-details">
<div class="row"> <div class="row">
<div class="col-lg-6"> <div class="col-lg-6">
@ -173,7 +173,125 @@
<div class="spinner-border text-light"></div> <div class="spinner-border text-light"></div>
</div> </div>
<!-- Stratum Job -->
<ng-container *ngIf="(job$ | async) as job;">
<h2 i18n="pool.next_block">Next block</h2>
<div class="box mb-3">
<div class="row" >
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td>
<table class="job-table table table-xs table-borderless table-fixed table-data">
<thead>
<tr>
<th class="data-title clip text-center height" i18n="latest-blocks.height">Height</th>
<th class="data-title clip text-center expected" i18n="next-block.expected-time">Expected</th>
<th class="data-title clip text-center reward" i18n="latest-blocks.reward">Reward</th>
<th class="data-title clip text-center timestamp" i18n="next-block.timestamp">Timestamp</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center height">
{{ job.height }}
</td>
<td class="text-center expected">
<ng-container *ngIf="(expectedBlockTime$ | async) as expectedBlockTime; else expectedPlaceholder">
<app-time kind="until" [time]="expectedBlockTime" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
</ng-container>
<ng-template #expectedPlaceholder>~</ng-template>
</td>
<td class="text-center reward">
<app-amount [satoshis]="job.reward"></app-amount>
</td>
<td class="text-center timestamp">
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="job.timestamp" [precision]="1" minUnit="minute" [hideTimeSince]="true"></app-timestamp>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td>
<table class="job-table table table-xs table-borderless table-fixed table-data">
<thead>
<tr>
<th class="data-title clip text-center coinbase" i18n="latest-blocks.coinbasetag">Coinbase tag</th>
<th class="data-title clip text-center clean" i18n="next-block.clean">Clean</th>
<th class="data-title clip text-center prevhash" i18n="next-block.prevhash">Prevhash</th>
<th class="data-title clip text-center job-received" i18n="next-block.job-received">Job Received</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center coinbase">
{{ job.scriptsig | hex2ascii }}
</td>
<td class="text-center clean">
@if (job.cleanJobs) {
<fa-icon [icon]="['fas', 'check-circle']" [fixedWidth]="true"></fa-icon>
} @else {
<fa-icon [icon]="['fas', 'times-circle']" [fixedWidth]="true"></fa-icon>
}
</td>
<td class="text-center prevhash">
<a [routerLink]="['/block' | relativeUrl, job.prevHash]">
<app-truncate [text]="job.prevHash" [lastChars]="8"></app-truncate>
</a>
</td>
<td class="text-center job-received">
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="job.received / 1000" [precision]="1" minUnit="minute" [hideTimeSince]="true"></app-timestamp>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td>
<table class="stratum-table">
<thead>
<tr>
<th class="data-title clip text-center" [attr.colspan]="Math.max(job.merkleBranches.length, 12)">
<a class="title-link" href="" [routerLink]="['/stratum' | relativeUrl]">
Merkle Branches
<span>&nbsp;</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 --> <!-- Blocks list -->
<h2 i18n="master-page.blocks">Blocks</h2>
<table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5" <table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5"
[infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()"> [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()">
<ng-container *ngIf="blocks$ | async as blocks; else skeleton"> <ng-container *ngIf="blocks$ | async as blocks; else skeleton">

View File

@ -49,111 +49,110 @@ div.scrollable {
max-height: 75px; max-height: 75px;
} }
.box { .pool-details {
padding-bottom: 5px;
@media (min-width: 767.98px) { @media (min-width: 767.98px) {
min-height: 187px; min-height: 187px;
} }
}
.label { .label {
width: 25%; width: 25%;
@media (min-width: 767.98px) { @media (min-width: 767.98px) {
vertical-align: middle; vertical-align: middle;
}
@media (max-width: 767.98px) {
font-weight: bold;
}
} }
@media (max-width: 767.98px) { .label.addresses {
font-weight: bold; vertical-align: top;
padding-top: 25px;
}
.addresses-data {
vertical-align: top;
font-family: monospace;
font-size: 14px;
} }
}
.label.addresses {
vertical-align: top;
padding-top: 25px;
}
.addresses-data {
vertical-align: top;
font-family: monospace;
font-size: 14px;
}
.data { .data {
text-align: right;
padding-left: 5%;
@media (max-width: 992px) {
text-align: left;
padding-left: 12px;
}
@media (max-width: 450px) {
text-align: right; text-align: right;
padding-left: 5%;
@media (max-width: 992px) {
text-align: left;
padding-left: 12px;
}
@media (max-width: 450px) {
text-align: right;
}
} }
}
.progress { .progress {
background-color: var(--secondary); background-color: var(--secondary);
} }
.coinbase { .coinbase {
width: 20%;
@media (max-width: 875px) {
display: none;
}
}
.height {
width: 10%;
}
.timestamp {
@media (max-width: 875px) {
padding-left: 50px;
}
@media (max-width: 685px) {
display: none;
}
}
.mined {
width: 13%;
@media (max-width: 1100px) {
display: none;
}
}
.txs {
padding-right: 40px;
@media (max-width: 1100px) {
padding-right: 10px;
}
@media (max-width: 875px) {
padding-right: 20px;
}
@media (max-width: 567px) {
padding-right: 10px;
}
}
.size {
width: 12%;
@media (max-width: 1000px) {
width: 15%;
}
@media (max-width: 875px) {
width: 20%; width: 20%;
@media (max-width: 875px) {
display: none;
}
} }
@media (max-width: 650px) {
width: 20%;
}
@media (max-width: 450px) {
display: none;
}
}
.scriptmessage { .height {
overflow: hidden; width: 10%;
display: inline-block; }
text-overflow: ellipsis;
vertical-align: middle; .timestamp {
width: auto; @media (max-width: 875px) {
text-align: left; padding-left: 50px;
}
@media (max-width: 685px) {
display: none;
}
}
.mined {
width: 13%;
@media (max-width: 1100px) {
display: none;
}
}
.txs {
padding-right: 40px;
@media (max-width: 1100px) {
padding-right: 10px;
}
@media (max-width: 875px) {
padding-right: 20px;
}
@media (max-width: 567px) {
padding-right: 10px;
}
}
.size {
width: 12%;
@media (max-width: 1000px) {
width: 15%;
}
@media (max-width: 875px) {
width: 20%;
}
@media (max-width: 650px) {
width: 20%;
}
@media (max-width: 450px) {
display: none;
}
}
.scriptmessage {
overflow: hidden;
display: inline-block;
text-overflow: ellipsis;
vertical-align: middle;
width: auto;
text-align: left;
}
} }
.skeleton-loader { .skeleton-loader {
@ -214,4 +213,69 @@ div.scrollable {
.taller-row { .taller-row {
height: 75px; 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;
} }

View File

@ -10,6 +10,9 @@ import { selectPowerOfTen } from '@app/bitcoin.utils';
import { formatNumber } from '@angular/common'; import { formatNumber } from '@angular/common';
import { SeoService } from '@app/services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { StratumJob } from '../../interfaces/websocket.interface';
import { WebsocketService } from '../../services/websocket.service';
import { MiningService } from '../../services/mining.service';
interface AccelerationTotal { interface AccelerationTotal {
cost: number, cost: number,
@ -27,12 +30,16 @@ export class PoolComponent implements OnInit {
@Input() left: number | string = 75; @Input() left: number | string = 75;
gfg = true; gfg = true;
stratumEnabled = this.stateService.env.STRATUM_ENABLED;
formatNumber = formatNumber; formatNumber = formatNumber;
Math = Math;
slugSubscription: Subscription; slugSubscription: Subscription;
poolStats$: Observable<PoolStat>; poolStats$: Observable<PoolStat>;
blocks$: Observable<BlockExtended[]>; blocks$: Observable<BlockExtended[]>;
oobFees$: Observable<AccelerationTotal[]>; oobFees$: Observable<AccelerationTotal[]>;
job$: Observable<StratumJob | null>;
expectedBlockTime$: Observable<number>;
isLoading = true; isLoading = true;
error: HttpErrorResponse | null = null; error: HttpErrorResponse | null = null;
@ -53,6 +60,8 @@ export class PoolComponent implements OnInit {
private apiService: ApiService, private apiService: ApiService,
private route: ActivatedRoute, private route: ActivatedRoute,
public stateService: StateService, public stateService: StateService,
private websocketService: WebsocketService,
private miningService: MiningService,
private seoService: SeoService, private seoService: SeoService,
) { ) {
this.auditAvailable = this.stateService.env.AUDIT; this.auditAvailable = this.stateService.env.AUDIT;
@ -62,7 +71,7 @@ export class PoolComponent implements OnInit {
this.slugSubscription = this.route.params.pipe(map((params) => params.slug)).subscribe((slug) => { this.slugSubscription = this.route.params.pipe(map((params) => params.slug)).subscribe((slug) => {
this.isLoading = true; this.isLoading = true;
this.blocks = []; this.blocks = [];
this.chartOptions = {}; this.chartOptions = {};
this.slug = slug; this.slug = slug;
this.initializeObservables(); this.initializeObservables();
}); });
@ -129,6 +138,31 @@ export class PoolComponent implements OnInit {
}), }),
filter(oob => oob.length === 3 && oob[2].count > 0) filter(oob => oob.length === 3 && oob[2].count > 0)
); );
if (this.stratumEnabled) {
this.job$ = combineLatest([
this.poolStats$.pipe(
tap((poolStats) => {
this.websocketService.startTrackStratum(poolStats.pool.unique_id);
})
),
this.stateService.stratumJobs$
]).pipe(
map(([poolStats, jobs]) => {
return jobs[poolStats.pool.unique_id];
})
);
this.expectedBlockTime$ = combineLatest([
this.miningService.getMiningStats('1w'),
this.poolStats$,
this.stateService.difficultyAdjustment$
]).pipe(
map(([miningStats, poolStat, da]) => {
return (da.timeAvg / ((poolStat.estimatedHashrate || 0) / (miningStats.lastEstimatedHashrate * 1_000_000_000_000_000_000))) + Date.now() + da.timeOffset;
})
);
}
} }
prepareChartOptions(hashrate, share) { prepareChartOptions(hashrate, share) {
@ -327,6 +361,10 @@ export class PoolComponent implements OnInit {
return block.height; return block.height;
} }
reverseHash(hash: string) {
return hash.match(/../g).reverse().join('');
}
ngOnDestroy(): void { ngOnDestroy(): void {
this.slugSubscription.unsubscribe(); this.slugSubscription.unsubscribe();
} }

View File

@ -82,6 +82,10 @@ export class ServerHealthComponent implements OnInit {
return '🇺🇸'; return '🇺🇸';
} else if (host.includes('.va1.')) { } else if (host.includes('.va1.')) {
return '🇺🇸'; return '🇺🇸';
} else if (host.includes('.sg1.')) {
return '🇸🇬';
} else if (host.includes('.hnl.')) {
return '🤙';
} else { } else {
return ''; return '';
} }

View File

@ -0,0 +1,55 @@
<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>

View File

@ -0,0 +1,138 @@
.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;
}

View File

@ -0,0 +1,230 @@
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 '│';
}
}
}

View File

@ -1,4 +1,47 @@
<ng-container [ngSwitch]="name"> <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'"> <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"> <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"/> <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"/>

View File

@ -8,7 +8,7 @@
</a> </a>
} @else if (enterpriseInfo?.img || enterpriseInfo?.imageMd5) { } @else if (enterpriseInfo?.img || enterpriseInfo?.imageMd5) {
<a [routerLink]="['/' | relativeUrl]"> <a [routerLink]="['/' | relativeUrl]">
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + enterpriseInfo.name + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}"> <img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + enterpriseInfo.name + '/logo'" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
</a> </a>
<div class="vertical-line"></div> <div class="vertical-line"></div>
} }
@ -124,7 +124,6 @@
<ng-container *ngIf="(ETA$ | async) as eta;"> <ng-container *ngIf="(ETA$ | async) as eta;">
<app-accelerate-checkout <app-accelerate-checkout
*ngIf="(da$ | async) as da;" *ngIf="(da$ | async) as da;"
[cashappEnabled]="cashappEligible"
[advancedEnabled]="false" [advancedEnabled]="false"
[forceMobile]="true" [forceMobile]="true"
[tx]="tx" [tx]="tx"

View File

@ -756,10 +756,6 @@ export class TrackerComponent implements OnInit, OnDestroy {
} }
} }
get cashappEligible(): boolean {
return this.mempoolPosition?.block > 0 && this.tx.weight < 4000;
}
get showAccelerationSummary(): boolean { get showAccelerationSummary(): boolean {
return ( return (
this.tx this.tx

View File

@ -139,7 +139,6 @@
<app-accelerate-checkout <app-accelerate-checkout
*ngIf="(da$ | async) as da;" *ngIf="(da$ | async) as da;"
[cashappEnabled]="cashappEligible"
[advancedEnabled]="true" [advancedEnabled]="true"
[tx]="tx" [tx]="tx"
[accelerating]="isAcceleration" [accelerating]="isAcceleration"

View File

@ -156,7 +156,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
showAccelerationDetails = false; showAccelerationDetails = false;
hasAccelerationDetails = false; hasAccelerationDetails = false;
scrollIntoAccelPreview = false; scrollIntoAccelPreview = false;
cashappEligible = false;
auditEnabled: boolean = this.stateService.env.AUDIT && this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true; auditEnabled: boolean = this.stateService.env.AUDIT && this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true;
isMempoolSpaceBuild = this.stateService.isMempoolSpaceBuild; isMempoolSpaceBuild = this.stateService.isMempoolSpaceBuild;
@ -528,9 +527,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.miningStats = stats; 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) { if (!this.gotInitialPosition && txPosition.position?.block === 0 && txPosition.position?.vsize < 750_000) {
this.accelerationFlowCompleted = true; this.accelerationFlowCompleted = true;
} }
@ -1036,7 +1032,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.showAccelerationDetails = false; this.showAccelerationDetails = false;
this.accelerationFlowCompleted = false; this.accelerationFlowCompleted = false;
this.accelerationInfo = null; this.accelerationInfo = null;
this.cashappEligible = false;
this.txInBlockIndex = null; this.txInBlockIndex = null;
this.mempoolPosition = null; this.mempoolPosition = null;
this.pool = null; this.pool = null;

View File

@ -1,6 +1,6 @@
<a href="#" (click)="twitterLogin()" <a href="#" (click)="twitterLogin()"
[class]="(disabled ? 'disabled': '') + (customClass ? customClass : 'w-100 btn mt-1 d-flex justify-content-center align-items-center')" [class]="(disabled ? 'disabled': '') + (customClass ? customClass : 'w-100 btn mt-1 d-flex justify-content-center align-items-center')"
style="background-color: #1DA1F2" [style]="width ? 'width: ' + width : ''"> style="background-color: rgb(31, 35, 40)" [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> <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> </a>

View File

@ -412,13 +412,13 @@ export interface Acceleration {
feeDelta: number; feeDelta: number;
blockHash: string; blockHash: string;
blockHeight: number; blockHeight: number;
acceleratedFeeRate?: number; acceleratedFeeRate?: number;
boost?: number; boost?: number;
bidBoost?: number; bidBoost?: number;
boostCost?: number; boostCost?: number;
boostRate?: number; boostRate?: number;
minedByPoolUniqueId?: number; minedByPoolUniqueId?: number;
canceled?: number;
} }
export interface AccelerationHistoryParams { export interface AccelerationHistoryParams {

View File

@ -21,6 +21,8 @@ export interface WebsocketResponse {
rbfInfo?: RbfTree; rbfInfo?: RbfTree;
rbfLatest?: RbfTree[]; rbfLatest?: RbfTree[];
rbfLatestSummary?: ReplacementInfo[]; rbfLatestSummary?: ReplacementInfo[];
stratumJob?: StratumJob;
stratumJobs?: Record<number, StratumJob>;
utxoSpent?: object; utxoSpent?: object;
transactions?: TransactionStripped[]; transactions?: TransactionStripped[];
loadingIndicators?: ILoadingIndicators; loadingIndicators?: ILoadingIndicators;
@ -37,6 +39,7 @@ export interface WebsocketResponse {
'track-rbf-summary'?: boolean; 'track-rbf-summary'?: boolean;
'track-accelerations'?: boolean; 'track-accelerations'?: boolean;
'track-wallet'?: string; 'track-wallet'?: string;
'track-stratum'?: string | number;
'watch-mempool'?: boolean; 'watch-mempool'?: boolean;
'refresh-blocks'?: boolean; 'refresh-blocks'?: boolean;
} }
@ -150,3 +153,24 @@ export interface HealthCheckHost {
electrs?: string; electrs?: string;
} }
} }
export interface StratumJob {
pool: number;
height: number;
coinbase: string;
scriptsig: string;
reward: number;
jobId: string;
extraNonce: string;
extraNonce2Size: number;
prevHash: string;
coinbase1: string;
coinbase2: string;
merkleBranches: string[];
version: string;
bits: string;
time: string;
timestamp: number;
cleanJobs: boolean;
received: number;
}

View File

@ -10,9 +10,10 @@ import { TestTransactionsComponent } from '@components/test-transactions/test-tr
import { CalculatorComponent } from '@components/calculator/calculator.component'; import { CalculatorComponent } from '@components/calculator/calculator.component';
import { BlocksList } from '@components/blocks-list/blocks-list.component'; import { BlocksList } from '@components/blocks-list/blocks-list.component';
import { RbfList } from '@components/rbf-list/rbf-list.component'; import { RbfList } from '@components/rbf-list/rbf-list.component';
import { StratumList } from '@components/stratum/stratum-list/stratum-list.component';
import { ServerHealthComponent } from '@components/server-health/server-health.component'; import { ServerHealthComponent } from '@components/server-health/server-health.component';
import { ServerStatusComponent } from '@components/server-health/server-status.component'; import { ServerStatusComponent } from '@components/server-health/server-status.component';
import { FaucetComponent } from '@components/faucet/faucet.component' import { FaucetComponent } from '@components/faucet/faucet.component';
const browserWindow = window || {}; const browserWindow = window || {};
// @ts-ignore // @ts-ignore
@ -56,6 +57,16 @@ const routes: Routes = [
path: 'rbf', path: 'rbf',
component: RbfList, component: RbfList,
}, },
...(browserWindowEnv.STRATUM_ENABLED ? [{
path: 'stratum',
component: StartComponent,
children: [
{
path: '',
component: StratumList,
}
]
}] : []),
{ {
path: 'terms-of-service', path: 'terms-of-service',
loadChildren: () => import('@components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule), loadChildren: () => import('@components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule),

View File

@ -58,6 +58,7 @@ export class AuthServiceMempool {
setAuth(auth: any) { setAuth(auth: any) {
if (!auth) { if (!auth) {
localStorage.removeItem('auth'); localStorage.removeItem('auth');
localStorage.removeItem('authenticatorStatus');
} else { } else {
localStorage.setItem('auth', JSON.stringify(auth)); localStorage.setItem('auth', JSON.stringify(auth));
} }

View File

@ -64,8 +64,8 @@ export class MiningService {
); );
} }
} }
/** /**
* Get names and slugs of all pools * Get names and slugs of all pools
*/ */
public getPools(): Observable<any[]> { public getPools(): Observable<any[]> {
@ -75,7 +75,6 @@ export class MiningService {
return this.poolsData; return this.poolsData;
}) })
); );
} }
/** /**
* Set the hashrate power of ten we want to display * Set the hashrate power of ten we want to display

View File

@ -146,6 +146,10 @@ export class ServicesApiServices {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD, userChallenged: userChallenged }); return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD, userChallenged: userChallenged });
} }
accelerateWithCardOnFile$(txInput: string, token: string, verificationToken: string, referenceId: string, userApprovedUSD: number, userChallenged: boolean) {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cardOnFile`, { txInput: txInput, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD, userChallenged: userChallenged });
}
getAccelerations$(): Observable<Acceleration[]> { getAccelerations$(): Observable<Acceleration[]> {
return this.httpClient.get<Acceleration[]>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations`); return this.httpClient.get<Acceleration[]>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations`);
} }

View File

@ -1,7 +1,7 @@
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core'; import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs'; import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
import { AddressTxSummary, Transaction } from '@interfaces/electrs.interface'; import { Transaction } from '@interfaces/electrs.interface';
import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, isMempoolState } from '@interfaces/websocket.interface'; import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, StratumJob, isMempoolState } from '@interfaces/websocket.interface';
import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '@interfaces/node-api.interface'; import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '@interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router'; import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common'; import { isPlatformBrowser } from '@angular/common';
@ -81,6 +81,7 @@ export interface Env {
ADDITIONAL_CURRENCIES: boolean; ADDITIONAL_CURRENCIES: boolean;
GIT_COMMIT_HASH_MEMPOOL_SPACE?: string; GIT_COMMIT_HASH_MEMPOOL_SPACE?: string;
PACKAGE_JSON_VERSION_MEMPOOL_SPACE?: string; PACKAGE_JSON_VERSION_MEMPOOL_SPACE?: string;
STRATUM_ENABLED: boolean;
SERVICES_API?: string; SERVICES_API?: string;
customize?: Customization; customize?: Customization;
PROD_DOMAINS: string[]; PROD_DOMAINS: string[];
@ -123,6 +124,7 @@ const defaultEnv: Env = {
'ACCELERATOR_BUTTON': true, 'ACCELERATOR_BUTTON': true,
'PUBLIC_ACCELERATIONS': false, 'PUBLIC_ACCELERATIONS': false,
'ADDITIONAL_CURRENCIES': false, 'ADDITIONAL_CURRENCIES': false,
'STRATUM_ENABLED': false,
'SERVICES_API': 'https://mempool.space/api/v1/services', 'SERVICES_API': 'https://mempool.space/api/v1/services',
'PROD_DOMAINS': [], 'PROD_DOMAINS': [],
}; };
@ -159,6 +161,8 @@ export class StateService {
liveMempoolBlockTransactions$: Observable<{ block: number, transactions: { [txid: string]: TransactionStripped} }>; liveMempoolBlockTransactions$: Observable<{ block: number, transactions: { [txid: string]: TransactionStripped} }>;
accelerations$ = new Subject<AccelerationDelta>(); accelerations$ = new Subject<AccelerationDelta>();
liveAccelerations$: Observable<Acceleration[]>; liveAccelerations$: Observable<Acceleration[]>;
stratumJobUpdate$ = new Subject<{ state: Record<string, StratumJob> } | { job: StratumJob }>();
stratumJobs$ = new BehaviorSubject<Record<string, StratumJob>>({});
txConfirmed$ = new Subject<[string, BlockExtended]>(); txConfirmed$ = new Subject<[string, BlockExtended]>();
txReplaced$ = new Subject<ReplacedTransaction>(); txReplaced$ = new Subject<ReplacedTransaction>();
txRbfInfo$ = new Subject<RbfTree>(); txRbfInfo$ = new Subject<RbfTree>();
@ -303,6 +307,24 @@ export class StateService {
map((accMap) => Object.values(accMap).sort((a,b) => b.added - a.added)) map((accMap) => Object.values(accMap).sort((a,b) => b.added - a.added))
); );
this.stratumJobUpdate$.pipe(
scan((acc: Record<string, StratumJob>, update: { state: Record<string, StratumJob> } | { job: StratumJob }) => {
if ('state' in update) {
// Replace the entire state
return update.state;
} else {
// Update or create a single job entry
return {
...acc,
[update.job.pool]: update.job
};
}
}, {}),
shareReplay(1)
).subscribe(val => {
this.stratumJobs$.next(val);
});
this.networkChanged$.subscribe((network) => { this.networkChanged$.subscribe((network) => {
this.transactions$ = new BehaviorSubject<TransactionStripped[]>(null); this.transactions$ = new BehaviorSubject<TransactionStripped[]>(null);
this.blocksSubject$.next([]); this.blocksSubject$.next([]);

View File

@ -36,6 +36,7 @@ export class WebsocketService {
private isTrackingAccelerations: boolean = false; private isTrackingAccelerations: boolean = false;
private isTrackingWallet: boolean = false; private isTrackingWallet: boolean = false;
private trackingWalletName: string; private trackingWalletName: string;
private isTrackingStratum: string | number | false = false;
private trackingMempoolBlock: number; private trackingMempoolBlock: number;
private trackingMempoolBlockNetwork: string; private trackingMempoolBlockNetwork: string;
private stoppingTrackMempoolBlock: any | null = null; private stoppingTrackMempoolBlock: any | null = null;
@ -143,6 +144,9 @@ export class WebsocketService {
if (this.isTrackingWallet) { if (this.isTrackingWallet) {
this.startTrackingWallet(this.trackingWalletName); this.startTrackingWallet(this.trackingWalletName);
} }
if (this.isTrackingStratum !== false) {
this.startTrackStratum(this.isTrackingStratum);
}
this.stateService.connectionState$.next(2); this.stateService.connectionState$.next(2);
} }
@ -289,6 +293,18 @@ export class WebsocketService {
} }
} }
startTrackStratum(pool: number | string) {
this.websocketSubject.next({ 'track-stratum': pool });
this.isTrackingStratum = pool;
}
stopTrackStratum() {
if (this.isTrackingStratum) {
this.websocketSubject.next({ 'track-stratum': null });
this.isTrackingStratum = false;
}
}
fetchStatistics(historicalDate: string) { fetchStatistics(historicalDate: string) {
this.websocketSubject.next({ historicalDate }); this.websocketSubject.next({ historicalDate });
} }
@ -512,6 +528,14 @@ export class WebsocketService {
this.stateService.previousRetarget$.next(response.previousRetarget); this.stateService.previousRetarget$.next(response.previousRetarget);
} }
if (response.stratumJobs) {
this.stateService.stratumJobUpdate$.next({ state: response.stratumJobs });
}
if (response.stratumJob) {
this.stateService.stratumJobUpdate$.next({ job: response.stratumJob });
}
if (response['tomahawk']) { if (response['tomahawk']) {
this.stateService.serverHealth$.next(response['tomahawk']); this.stateService.serverHealth$.next(response['tomahawk']);
} }

View File

@ -26,6 +26,7 @@ export const MempoolErrors = {
'unauthorized': `You are not authorized to do this`, 'unauthorized': `You are not authorized to do this`,
'faucet_too_soon': `You cannot request any more coins right now. Try again later.`, 'faucet_too_soon': `You cannot request any more coins right now. Try again later.`,
'faucet_not_available': `The faucet is not available right now. Try again later.`, 'faucet_not_available': `The faucet is not available right now. Try again later.`,
'faucet_not_available_no_utxo': `The faucet is not available right now. Please try again once a new block has been mined.`,
'faucet_maximum_reached': `You are not allowed to request more coins`, 'faucet_maximum_reached': `You are not allowed to request more coins`,
'faucet_address_not_allowed': `You cannot use this address`, 'faucet_address_not_allowed': `You cannot use this address`,
'faucet_below_minimum': `Requested amount is too small`, 'faucet_below_minimum': `Requested amount is too small`,

View File

@ -4,7 +4,10 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome'; import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle, import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown, faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, faCircleXmark, faCalendarCheck, faMoneyBillTrendUp } from '@fortawesome/free-solid-svg-icons'; faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft,
faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck,
faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline,
faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faShareNodes, faCreditCard } from '@fortawesome/free-solid-svg-icons';
import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { MenuComponent } from '@components/menu/menu.component'; import { MenuComponent } from '@components/menu/menu.component';
import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component'; import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component';
@ -80,6 +83,7 @@ import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe';
import { DifficultyAdjustmentsTable } from '@components/difficulty-adjustments-table/difficulty-adjustments-table.components'; import { DifficultyAdjustmentsTable } from '@components/difficulty-adjustments-table/difficulty-adjustments-table.components';
import { BlocksList } from '@components/blocks-list/blocks-list.component'; import { BlocksList } from '@components/blocks-list/blocks-list.component';
import { RbfList } from '@components/rbf-list/rbf-list.component'; import { RbfList } from '@components/rbf-list/rbf-list.component';
import { StratumList } from '@components/stratum/stratum-list/stratum-list.component';
import { RewardStatsComponent } from '@components/reward-stats/reward-stats.component'; import { RewardStatsComponent } from '@components/reward-stats/reward-stats.component';
import { DataCyDirective } from '@app/data-cy.directive'; import { DataCyDirective } from '@app/data-cy.directive';
import { LoadingIndicatorComponent } from '@components/loading-indicator/loading-indicator.component'; import { LoadingIndicatorComponent } from '@components/loading-indicator/loading-indicator.component';
@ -121,6 +125,7 @@ import { TwitterLogin } from '@components/twitter-login/twitter-login.component'
import { BitcoinInvoiceComponent } from '@components/bitcoin-invoice/bitcoin-invoice.component'; import { BitcoinInvoiceComponent } from '@components/bitcoin-invoice/bitcoin-invoice.component';
import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/weight-directives/weight-directives'; import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/weight-directives/weight-directives';
import { GithubLogin } from '../components/github-login.component/github-login.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -198,6 +203,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
DifficultyAdjustmentsTable, DifficultyAdjustmentsTable,
BlocksList, BlocksList,
RbfList, RbfList,
StratumList,
DataCyDirective, DataCyDirective,
RewardStatsComponent, RewardStatsComponent,
LoadingIndicatorComponent, LoadingIndicatorComponent,
@ -237,6 +243,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
TwitterWidgetComponent, TwitterWidgetComponent,
FaucetComponent, FaucetComponent,
TwitterLogin, TwitterLogin,
GithubLogin,
BitcoinInvoiceComponent, BitcoinInvoiceComponent,
], ],
imports: [ imports: [
@ -342,6 +349,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
AmountShortenerPipe, AmountShortenerPipe,
DifficultyAdjustmentsTable, DifficultyAdjustmentsTable,
BlocksList, BlocksList,
StratumList,
DataCyDirective, DataCyDirective,
RewardStatsComponent, RewardStatsComponent,
LoadingIndicatorComponent, LoadingIndicatorComponent,
@ -370,6 +378,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
HttpErrorComponent, HttpErrorComponent,
TwitterWidgetComponent, TwitterWidgetComponent,
TwitterLogin, TwitterLogin,
GithubLogin,
BitcoinInvoiceComponent, BitcoinInvoiceComponent,
BitcoinsatoshisPipe, BitcoinsatoshisPipe,
@ -452,5 +461,8 @@ export class SharedModule {
library.addIcons(faCircleXmark); library.addIcons(faCircleXmark);
library.addIcons(faCalendarCheck); library.addIcons(faCalendarCheck);
library.addIcons(faMoneyBillTrendUp); library.addIcons(faMoneyBillTrendUp);
library.addIcons(faRobot);
library.addIcons(faShareNodes);
library.addIcons(faCreditCard);
} }
} }

View File

@ -150,7 +150,8 @@ http {
} }
server { server {
listen 127.0.0.1:80; listen 80;
listen [::]:80
include /etc/nginx/nginx-mempool.conf; include /etc/nginx/nginx-mempool.conf;
} }
} }

View File

@ -15,6 +15,7 @@ whitelist=127.0.0.1
whitelist=103.99.168.0/22 whitelist=103.99.168.0/22
whitelist=2401:b140::/32 whitelist=2401:b140::/32
blocksxor=0 blocksxor=0
logtimemicros=1
#uacomment=@wiz #uacomment=@wiz
[main] [main]

View File

@ -26,12 +26,14 @@
"ESPLORA": { "ESPLORA": {
"UNIX_SOCKET_PATH": "/elements/socket/esplora-elements-liquid", "UNIX_SOCKET_PATH": "/elements/socket/esplora-elements-liquid",
"FALLBACK": [ "FALLBACK": [
"http://node201.fmt.mempool.space:3001", "http://node201.hnl.mempool.space:3001",
"http://node202.fmt.mempool.space:3001", "http://node202.hnl.mempool.space:3001",
"http://node203.fmt.mempool.space:3001", "http://node203.hnl.mempool.space:3001",
"http://node204.fmt.mempool.space:3001", "http://node204.hnl.mempool.space:3001",
"http://node205.fmt.mempool.space:3001", "http://node201.sg1.mempool.space:3001",
"http://node206.fmt.mempool.space:3001", "http://node202.sg1.mempool.space:3001",
"http://node203.sg1.mempool.space:3001",
"http://node204.sg1.mempool.space:3001",
"http://node201.va1.mempool.space:3001", "http://node201.va1.mempool.space:3001",
"http://node202.va1.mempool.space:3001", "http://node202.va1.mempool.space:3001",
"http://node203.va1.mempool.space:3001", "http://node203.va1.mempool.space:3001",

View File

@ -26,12 +26,14 @@
"ESPLORA": { "ESPLORA": {
"UNIX_SOCKET_PATH": "/elements/socket/esplora-elements-liquidtestnet", "UNIX_SOCKET_PATH": "/elements/socket/esplora-elements-liquidtestnet",
"FALLBACK": [ "FALLBACK": [
"http://node201.fmt.mempool.space:3004", "http://node201.hnl.mempool.space:3004",
"http://node202.fmt.mempool.space:3004", "http://node202.hnl.mempool.space:3004",
"http://node203.fmt.mempool.space:3004", "http://node203.hnl.mempool.space:3004",
"http://node204.fmt.mempool.space:3004", "http://node204.hnl.mempool.space:3004",
"http://node205.fmt.mempool.space:3004", "http://node201.sg1.mempool.space:3004",
"http://node206.fmt.mempool.space:3004", "http://node202.sg1.mempool.space:3004",
"http://node203.sg1.mempool.space:3004",
"http://node204.sg1.mempool.space:3004",
"http://node201.va1.mempool.space:3004", "http://node201.va1.mempool.space:3004",
"http://node202.va1.mempool.space:3004", "http://node202.va1.mempool.space:3004",
"http://node203.va1.mempool.space:3004", "http://node203.va1.mempool.space:3004",

View File

@ -19,12 +19,14 @@
"ESPLORA": { "ESPLORA": {
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-mainnet", "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-mainnet",
"FALLBACK": [ "FALLBACK": [
"http://node201.fmt.mempool.space:3000", "http://node201.hnl.mempool.space:3000",
"http://node202.fmt.mempool.space:3000", "http://node202.hnl.mempool.space:3000",
"http://node203.fmt.mempool.space:3000", "http://node203.hnl.mempool.space:3000",
"http://node204.fmt.mempool.space:3000", "http://node204.hnl.mempool.space:3000",
"http://node205.fmt.mempool.space:3000", "http://node201.sg1.mempool.space:3000",
"http://node206.fmt.mempool.space:3000", "http://node202.sg1.mempool.space:3000",
"http://node203.sg1.mempool.space:3000",
"http://node204.sg1.mempool.space:3000",
"http://node201.va1.mempool.space:3000", "http://node201.va1.mempool.space:3000",
"http://node202.va1.mempool.space:3000", "http://node202.va1.mempool.space:3000",
"http://node203.va1.mempool.space:3000", "http://node203.va1.mempool.space:3000",

View File

@ -30,7 +30,8 @@
"CORE_RPC": { "CORE_RPC": {
"PORT": 8332, "PORT": 8332,
"USERNAME": "__BITCOIN_RPC_USER__", "USERNAME": "__BITCOIN_RPC_USER__",
"PASSWORD": "__BITCOIN_RPC_PASS__" "PASSWORD": "__BITCOIN_RPC_PASS__",
"DEBUG_LOG_PATH": "/bitcoin/debug.log"
}, },
"SECOND_CORE_RPC": { "SECOND_CORE_RPC": {
"PORT": 8302, "PORT": 8302,
@ -40,12 +41,14 @@
"ESPLORA": { "ESPLORA": {
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-mainnet", "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-mainnet",
"FALLBACK": [ "FALLBACK": [
"http://node201.fmt.mempool.space:3000", "http://node201.hnl.mempool.space:3000",
"http://node202.fmt.mempool.space:3000", "http://node202.hnl.mempool.space:3000",
"http://node203.fmt.mempool.space:3000", "http://node203.hnl.mempool.space:3000",
"http://node204.fmt.mempool.space:3000", "http://node204.hnl.mempool.space:3000",
"http://node205.fmt.mempool.space:3000", "http://node201.sg1.mempool.space:3000",
"http://node206.fmt.mempool.space:3000", "http://node202.sg1.mempool.space:3000",
"http://node203.sg1.mempool.space:3000",
"http://node204.sg1.mempool.space:3000",
"http://node201.va1.mempool.space:3000", "http://node201.va1.mempool.space:3000",
"http://node202.va1.mempool.space:3000", "http://node202.va1.mempool.space:3000",
"http://node203.va1.mempool.space:3000", "http://node203.va1.mempool.space:3000",
@ -101,12 +104,14 @@
"STATISTICS": true, "STATISTICS": true,
"STATISTICS_START_TIME": "24h", "STATISTICS_START_TIME": "24h",
"SERVERS": [ "SERVERS": [
"node201.fmt.mempool.space", "node201.hnl.mempool.space",
"node202.fmt.mempool.space", "node202.hnl.mempool.space",
"node203.fmt.mempool.space", "node203.hnl.mempool.space",
"node204.fmt.mempool.space", "node204.hnl.mempool.space",
"node205.fmt.mempool.space", "node201.sg1.mempool.space",
"node206.fmt.mempool.space", "node202.sg1.mempool.space",
"node203.sg1.mempool.space",
"node204.sg1.mempool.space",
"node201.va1.mempool.space", "node201.va1.mempool.space",
"node202.va1.mempool.space", "node202.va1.mempool.space",
"node203.va1.mempool.space", "node203.va1.mempool.space",
@ -154,5 +159,9 @@
"WALLETS": { "WALLETS": {
"ENABLED": true, "ENABLED": true,
"WALLETS": ["BITB", "3350"] "WALLETS": ["BITB", "3350"]
},
"STRATUM": {
"ENABLED": true,
"API": "http://127.0.0.1:81/api/v1/stratum/ws"
} }
} }

View File

@ -19,12 +19,14 @@
"ESPLORA": { "ESPLORA": {
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-signet", "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-signet",
"FALLBACK": [ "FALLBACK": [
"http://node201.fmt.mempool.space:3003", "http://node201.hnl.mempool.space:3003",
"http://node202.fmt.mempool.space:3003", "http://node202.hnl.mempool.space:3003",
"http://node203.fmt.mempool.space:3003", "http://node203.hnl.mempool.space:3003",
"http://node204.fmt.mempool.space:3003", "http://node204.hnl.mempool.space:3003",
"http://node205.fmt.mempool.space:3003", "http://node201.sg1.mempool.space:3003",
"http://node206.fmt.mempool.space:3003", "http://node202.sg1.mempool.space:3003",
"http://node203.sg1.mempool.space:3003",
"http://node204.sg1.mempool.space:3003",
"http://node201.va1.mempool.space:3003", "http://node201.va1.mempool.space:3003",
"http://node202.va1.mempool.space:3003", "http://node202.va1.mempool.space:3003",
"http://node203.va1.mempool.space:3003", "http://node203.va1.mempool.space:3003",

View File

@ -28,12 +28,14 @@
"ESPLORA": { "ESPLORA": {
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-signet", "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-signet",
"FALLBACK": [ "FALLBACK": [
"http://node201.fmt.mempool.space:3003", "http://node201.hnl.mempool.space:3003",
"http://node202.fmt.mempool.space:3003", "http://node202.hnl.mempool.space:3003",
"http://node203.fmt.mempool.space:3003", "http://node203.hnl.mempool.space:3003",
"http://node204.fmt.mempool.space:3003", "http://node204.hnl.mempool.space:3003",
"http://node205.fmt.mempool.space:3003", "http://node201.sg1.mempool.space:3003",
"http://node206.fmt.mempool.space:3003", "http://node202.sg1.mempool.space:3003",
"http://node203.sg1.mempool.space:3003",
"http://node204.sg1.mempool.space:3003",
"http://node201.va1.mempool.space:3003", "http://node201.va1.mempool.space:3003",
"http://node202.va1.mempool.space:3003", "http://node202.va1.mempool.space:3003",
"http://node203.va1.mempool.space:3003", "http://node203.va1.mempool.space:3003",

View File

@ -19,12 +19,14 @@
"ESPLORA": { "ESPLORA": {
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-testnet", "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-testnet",
"FALLBACK": [ "FALLBACK": [
"http://node201.fmt.mempool.space:3002", "http://node201.hnl.mempool.space:3002",
"http://node202.fmt.mempool.space:3002", "http://node202.hnl.mempool.space:3002",
"http://node203.fmt.mempool.space:3002", "http://node203.hnl.mempool.space:3002",
"http://node204.fmt.mempool.space:3002", "http://node204.hnl.mempool.space:3002",
"http://node205.fmt.mempool.space:3002", "http://node201.sg1.mempool.space:3002",
"http://node206.fmt.mempool.space:3002", "http://node202.sg1.mempool.space:3002",
"http://node203.sg1.mempool.space:3002",
"http://node204.sg1.mempool.space:3002",
"http://node201.va1.mempool.space:3002", "http://node201.va1.mempool.space:3002",
"http://node202.va1.mempool.space:3002", "http://node202.va1.mempool.space:3002",
"http://node203.va1.mempool.space:3002", "http://node203.va1.mempool.space:3002",

View File

@ -28,12 +28,14 @@
"ESPLORA": { "ESPLORA": {
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-testnet", "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-testnet",
"FALLBACK": [ "FALLBACK": [
"http://node201.fmt.mempool.space:3002", "http://node201.hnl.mempool.space:3002",
"http://node202.fmt.mempool.space:3002", "http://node202.hnl.mempool.space:3002",
"http://node203.fmt.mempool.space:3002", "http://node203.hnl.mempool.space:3002",
"http://node204.fmt.mempool.space:3002", "http://node204.hnl.mempool.space:3002",
"http://node205.fmt.mempool.space:3002", "http://node201.sg1.mempool.space:3002",
"http://node206.fmt.mempool.space:3002", "http://node202.sg1.mempool.space:3002",
"http://node203.sg1.mempool.space:3002",
"http://node204.sg1.mempool.space:3002",
"http://node201.va1.mempool.space:3002", "http://node201.va1.mempool.space:3002",
"http://node202.va1.mempool.space:3002", "http://node202.va1.mempool.space:3002",
"http://node203.va1.mempool.space:3002", "http://node203.va1.mempool.space:3002",

View File

@ -28,12 +28,14 @@
"ESPLORA": { "ESPLORA": {
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-testnet4", "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-testnet4",
"FALLBACK": [ "FALLBACK": [
"http://node201.fmt.mempool.space:3005", "http://node201.hnl.mempool.space:3005",
"http://node202.fmt.mempool.space:3005", "http://node202.hnl.mempool.space:3005",
"http://node203.fmt.mempool.space:3005", "http://node203.hnl.mempool.space:3005",
"http://node204.fmt.mempool.space:3005", "http://node204.hnl.mempool.space:3005",
"http://node205.fmt.mempool.space:3005", "http://node201.sg1.mempool.space:3005",
"http://node206.fmt.mempool.space:3005", "http://node202.sg1.mempool.space:3005",
"http://node203.sg1.mempool.space:3005",
"http://node204.sg1.mempool.space:3005",
"http://node201.va1.mempool.space:3005", "http://node201.va1.mempool.space:3005",
"http://node202.va1.mempool.space:3005", "http://node202.va1.mempool.space:3005",
"http://node203.va1.mempool.space:3005", "http://node203.va1.mempool.space:3005",

View File

@ -4,8 +4,7 @@
"TESTNET4_ENABLED": true, "TESTNET4_ENABLED": true,
"LIQUID_ENABLED": false, "LIQUID_ENABLED": false,
"LIQUID_TESTNET_ENABLED": false, "LIQUID_TESTNET_ENABLED": false,
"BISQ_ENABLED": true, "STRATUM_ENABLED": true,
"BISQ_SEPARATE_BACKEND": true,
"SIGNET_ENABLED": true, "SIGNET_ENABLED": true,
"MEMPOOL_WEBSITE_URL": "https://mempool.space", "MEMPOOL_WEBSITE_URL": "https://mempool.space",
"LIQUID_WEBSITE_URL": "https://liquid.network", "LIQUID_WEBSITE_URL": "https://liquid.network",

View File

@ -2,7 +2,7 @@
proxy_cache_path /var/cache/nginx/services keys_zone=services:200m levels=1:2 inactive=30d max_size=200m; proxy_cache_path /var/cache/nginx/services keys_zone=services:200m levels=1:2 inactive=30d max_size=200m;
proxy_cache_path /var/cache/nginx/apihot keys_zone=apihot:200m levels=1:2 inactive=60m max_size=20m; proxy_cache_path /var/cache/nginx/apihot keys_zone=apihot:200m levels=1:2 inactive=60m max_size=20m;
proxy_cache_path /var/cache/nginx/apiwarm keys_zone=apiwarm:200m levels=1:2 inactive=24h max_size=200m; proxy_cache_path /var/cache/nginx/apiwarm keys_zone=apiwarm:200m levels=1:2 inactive=24h max_size=200m;
proxy_cache_path /var/cache/nginx/apinormal keys_zone=apinormal:200m levels=1:2 inactive=30d max_size=2000m; proxy_cache_path /var/cache/nginx/apinormal keys_zone=apinormal:500m levels=1:2 inactive=24h max_size=2000m;
proxy_cache_path /var/cache/nginx/apicold keys_zone=apicold:200m levels=1:2 inactive=60d max_size=2000m; proxy_cache_path /var/cache/nginx/apicold keys_zone=apicold:200m levels=1:2 inactive=60d max_size=2000m;
proxy_cache_path /var/cache/nginx/unfurler keys_zone=unfurler:200m levels=1:2 inactive=30d max_size=2000m; proxy_cache_path /var/cache/nginx/unfurler keys_zone=unfurler:200m levels=1:2 inactive=30d max_size=2000m;

189
rust/gbt/Cargo.lock generated
View File

@ -1,21 +1,21 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "addr2line" name = "addr2line"
version = "0.22.0" version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [ dependencies = [
"gimli", "gimli",
] ]
[[package]] [[package]]
name = "adler" name = "adler2"
version = "1.0.2" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
@ -28,48 +28,42 @@ dependencies = [
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.3.0" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.73" version = "0.3.74"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
dependencies = [ dependencies = [
"addr2line", "addr2line",
"cc",
"cfg-if", "cfg-if",
"libc", "libc",
"miniz_oxide", "miniz_oxide",
"object", "object",
"rustc-demangle", "rustc-demangle",
"windows-targets",
] ]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.6.0" version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.16.1" version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.6.1" version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
[[package]]
name = "cc"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "324c74f2155653c90b04f25b2a47a8a631360cb908f92a772695f430c7e31052"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@ -88,9 +82,9 @@ dependencies = [
[[package]] [[package]]
name = "ctor" name = "ctor"
version = "0.2.8" version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
dependencies = [ dependencies = [
"quote", "quote",
"syn", "syn",
@ -119,27 +113,21 @@ dependencies = [
[[package]] [[package]]
name = "gimli" name = "gimli"
version = "0.29.0" version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.14.5" version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.2.6" version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
@ -153,15 +141,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.155" version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]] [[package]]
name = "libloading" name = "libloading"
version = "0.8.4" version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-targets", "windows-targets",
@ -169,9 +157,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.22" version = "0.4.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
[[package]] [[package]]
name = "matchers" name = "matchers"
@ -190,18 +178,18 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.7.4" version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
dependencies = [ dependencies = [
"adler", "adler2",
] ]
[[package]] [[package]]
name = "napi" name = "napi"
version = "2.16.8" version = "2.16.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1bd081bbaef43600fd2c5dd4c525b8ecea7dfdacf40ebc674e87851dce6559e" checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"ctor", "ctor",
@ -213,15 +201,15 @@ dependencies = [
[[package]] [[package]]
name = "napi-build" name = "napi-build"
version = "2.1.3" version = "2.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a" checksum = "db836caddef23662b94e16bf1f26c40eceb09d6aee5d5b06a7ac199320b69b19"
[[package]] [[package]]
name = "napi-derive" name = "napi-derive"
version = "2.16.9" version = "2.16.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87c3b5d4ab13e20a4bb9d3a1e2f3d4e77eee4a205d0f810abfd226b971dc6ce5" checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"convert_case", "convert_case",
@ -233,9 +221,9 @@ dependencies = [
[[package]] [[package]]
name = "napi-derive-backend" name = "napi-derive-backend"
version = "1.0.71" version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de436a6ab93265beef838f8333c8345438f059df6081fe0ad0b8648ee0c524" checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf"
dependencies = [ dependencies = [
"convert_case", "convert_case",
"once_cell", "once_cell",
@ -265,30 +253,20 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"libc",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.36.1" version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.19.0" version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]] [[package]]
name = "overload" name = "overload"
@ -298,15 +276,15 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.14" version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]] [[package]]
name = "priority-queue" name = "priority-queue"
version = "2.0.3" version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70c501afe3a2e25c9bd219aa56ec1e04cdb3fcdd763055be268778c13fa82c1f" checksum = "714c75db297bc88a63783ffc6ab9f830698a6705aa0201416931759ef4c8183d"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"equivalent", "equivalent",
@ -315,32 +293,32 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.86" version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.36" version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.10.5" version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-automata 0.4.7", "regex-automata 0.4.9",
"regex-syntax 0.8.4", "regex-syntax 0.8.5",
] ]
[[package]] [[package]]
@ -354,13 +332,13 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.7" version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-syntax 0.8.4", "regex-syntax 0.8.5",
] ]
[[package]] [[package]]
@ -371,9 +349,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.4" version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
@ -383,9 +361,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.23" version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
[[package]] [[package]]
name = "sharded-slab" name = "sharded-slab"
@ -404,9 +382,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.71" version = "2.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -425,20 +403,19 @@ dependencies = [
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.38.1" version = "1.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"num_cpus",
"pin-project-lite", "pin-project-lite",
] ]
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.40" version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [ dependencies = [
"pin-project-lite", "pin-project-lite",
"tracing-attributes", "tracing-attributes",
@ -447,9 +424,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.27" version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -458,9 +435,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.32" version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"valuable", "valuable",
@ -479,9 +456,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-subscriber" name = "tracing-subscriber"
version = "0.3.18" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [ dependencies = [
"matchers", "matchers",
"nu-ansi-term", "nu-ansi-term",
@ -497,21 +474,21 @@ dependencies = [
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.12" version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243"
[[package]] [[package]]
name = "unicode-segmentation" name = "unicode-segmentation"
version = "1.11.0" version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.0" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "winapi" name = "winapi"

View File

@ -1 +1 @@
1.79 1.84