From 3db1486bfb1ac7a684c6d48691591e4adbe7591e Mon Sep 17 00:00:00 2001
From: nymkappa <1612910616@pm.me>
Date: Sun, 26 Mar 2023 14:35:10 +0900
Subject: [PATCH 001/639] Fix transaction amount overflow
---
.../transactions-list.component.html | 4 ++--
.../transactions-list.component.scss | 11 ++++++++++-
2 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html
index cb54e1870..1549f7871 100644
--- a/frontend/src/app/components/transactions-list/transactions-list.component.html
+++ b/frontend/src/app/components/transactions-list/transactions-list.component.html
@@ -77,7 +77,7 @@
-
+ 1000000}">
@@ -206,7 +206,7 @@
-
+ 1000000}">
diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.scss b/frontend/src/app/components/transactions-list/transactions-list.component.scss
index 08d7d7486..5d6dd7d61 100644
--- a/frontend/src/app/components/transactions-list/transactions-list.component.scss
+++ b/frontend/src/app/components/transactions-list/transactions-list.component.scss
@@ -46,7 +46,16 @@
}
td.amount {
- width: 32.5%;
+ width: 36%;
+ @media (max-width: 576px) {
+ width: 50%;
+ }
+}
+td.amount.large {
+ width: 45%;
+ @media (max-width: 576px) {
+ width: 60%;
+ }
}
.extra-info {
From 125b9e4525132304ca11c24eca871bd392adbc05 Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Wed, 24 May 2023 15:49:35 -0400
Subject: [PATCH 002/639] Restore liquid max block weight to defaults
---
production/mempool-frontend-config.liquid.json | 1 -
1 file changed, 1 deletion(-)
diff --git a/production/mempool-frontend-config.liquid.json b/production/mempool-frontend-config.liquid.json
index 6a7c79d52..1a4fc2998 100644
--- a/production/mempool-frontend-config.liquid.json
+++ b/production/mempool-frontend-config.liquid.json
@@ -11,7 +11,6 @@
"LIQUID_WEBSITE_URL": "https://liquid.network",
"BISQ_WEBSITE_URL": "https://bisq.markets",
"ITEMS_PER_PAGE": 25,
- "BLOCK_WEIGHT_UNITS": 300000,
"MEMPOOL_BLOCKS_AMOUNT": 2,
"KEEP_BLOCKS_AMOUNT": 16
}
From e9386ec003682ff921737709d234f4832822b0c8 Mon Sep 17 00:00:00 2001
From: Antoni Spaanderman <56turtle56@gmail.com>
Date: Sun, 26 Mar 2023 16:39:45 +0200
Subject: [PATCH 003/639] Add Bitcoin Core RPC cookie authentication option
---
backend/mempool-config.sample.json | 8 +++++--
.../__fixtures__/mempool-config.template.json | 10 ++++++---
backend/src/__tests__/config.test.ts | 8 +++++--
.../bitcoin/bitcoin-api-abstract-factory.ts | 1 +
backend/src/api/bitcoin/bitcoin-client.ts | 3 +++
.../src/api/bitcoin/bitcoin-second-client.ts | 3 +++
backend/src/config.ts | 8 +++++++
backend/src/rpc-api/jsonrpc.ts | 21 ++++++++++++++++---
docker/README.md | 16 ++++++++++----
docker/backend/mempool-config.json | 8 +++++--
docker/backend/start.sh | 8 +++++++
11 files changed, 78 insertions(+), 16 deletions(-)
diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json
index 32becd00d..68b5c3801 100644
--- a/backend/mempool-config.sample.json
+++ b/backend/mempool-config.sample.json
@@ -35,7 +35,9 @@
"PORT": 8332,
"USERNAME": "mempool",
"PASSWORD": "mempool",
- "TIMEOUT": 60000
+ "TIMEOUT": 60000,
+ "COOKIE": false,
+ "COOKIE_PATH": "/path/to/bitcoin/.cookie"
},
"ELECTRUM": {
"HOST": "127.0.0.1",
@@ -52,7 +54,9 @@
"PORT": 8332,
"USERNAME": "mempool",
"PASSWORD": "mempool",
- "TIMEOUT": 60000
+ "TIMEOUT": 60000,
+ "COOKIE": false,
+ "COOKIE_PATH": "/path/to/bitcoin/.cookie"
},
"DATABASE": {
"ENABLED": true,
diff --git a/backend/src/__fixtures__/mempool-config.template.json b/backend/src/__fixtures__/mempool-config.template.json
index 919784464..1d5c7135a 100644
--- a/backend/src/__fixtures__/mempool-config.template.json
+++ b/backend/src/__fixtures__/mempool-config.template.json
@@ -36,7 +36,9 @@
"PORT": 15,
"USERNAME": "__CORE_RPC_USERNAME__",
"PASSWORD": "__CORE_RPC_PASSWORD__",
- "TIMEOUT": 1000
+ "TIMEOUT": 1000,
+ "COOKIE": "__CORE_RPC_COOKIE__",
+ "COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
},
"ELECTRUM": {
"HOST": "__ELECTRUM_HOST__",
@@ -53,7 +55,9 @@
"PORT": 17,
"USERNAME": "__SECOND_CORE_RPC_USERNAME__",
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__",
- "TIMEOUT": 2000
+ "TIMEOUT": 2000,
+ "COOKIE": "__SECOND_CORE_RPC_COOKIE__",
+ "COOKIE_PATH": "__SECOND_CORE_RPC_COOKIE_PATH__"
},
"DATABASE": {
"ENABLED": false,
@@ -119,4 +123,4 @@
"CLIGHTNING": {
"SOCKET": "__CLIGHTNING_SOCKET__"
}
-}
\ No newline at end of file
+}
diff --git a/backend/src/__tests__/config.test.ts b/backend/src/__tests__/config.test.ts
index 278d83f50..f4cf719c6 100644
--- a/backend/src/__tests__/config.test.ts
+++ b/backend/src/__tests__/config.test.ts
@@ -54,7 +54,9 @@ describe('Mempool Backend Config', () => {
PORT: 8332,
USERNAME: 'mempool',
PASSWORD: 'mempool',
- TIMEOUT: 60000
+ TIMEOUT: 60000,
+ COOKIE: false,
+ COOKIE_PATH: ''
});
expect(config.SECOND_CORE_RPC).toStrictEqual({
@@ -62,7 +64,9 @@ describe('Mempool Backend Config', () => {
PORT: 8332,
USERNAME: 'mempool',
PASSWORD: 'mempool',
- TIMEOUT: 60000
+ TIMEOUT: 60000,
+ COOKIE: false,
+ COOKIE_PATH: ''
});
expect(config.DATABASE).toStrictEqual({
diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts
index 7b2802d1b..f8dfe8c26 100644
--- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts
+++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts
@@ -25,4 +25,5 @@ export interface BitcoinRpcCredentials {
user: string;
pass: string;
timeout: number;
+ cookie?: string;
}
diff --git a/backend/src/api/bitcoin/bitcoin-client.ts b/backend/src/api/bitcoin/bitcoin-client.ts
index 429638984..f0dab4441 100644
--- a/backend/src/api/bitcoin/bitcoin-client.ts
+++ b/backend/src/api/bitcoin/bitcoin-client.ts
@@ -2,12 +2,15 @@ import config from '../../config';
const bitcoin = require('../../rpc-api/index');
import { BitcoinRpcCredentials } from './bitcoin-api-abstract-factory';
+export const defaultCookiePath = `${process.env.HOME}/.bitcoin/${{mainnet:'',testnet:'testnet3/',signet:'signet/'}[config.MEMPOOL.NETWORK]}.cookie`;
+
const nodeRpcCredentials: BitcoinRpcCredentials = {
host: config.CORE_RPC.HOST,
port: config.CORE_RPC.PORT,
user: config.CORE_RPC.USERNAME,
pass: config.CORE_RPC.PASSWORD,
timeout: config.CORE_RPC.TIMEOUT,
+ cookie: config.CORE_RPC.COOKIE ? config.CORE_RPC.COOKIE_PATH || defaultCookiePath : undefined,
};
export default new bitcoin.Client(nodeRpcCredentials);
diff --git a/backend/src/api/bitcoin/bitcoin-second-client.ts b/backend/src/api/bitcoin/bitcoin-second-client.ts
index 7f81a96a0..85d05556e 100644
--- a/backend/src/api/bitcoin/bitcoin-second-client.ts
+++ b/backend/src/api/bitcoin/bitcoin-second-client.ts
@@ -2,12 +2,15 @@ import config from '../../config';
const bitcoin = require('../../rpc-api/index');
import { BitcoinRpcCredentials } from './bitcoin-api-abstract-factory';
+import { defaultCookiePath } from './bitcoin-client';
+
const nodeRpcCredentials: BitcoinRpcCredentials = {
host: config.SECOND_CORE_RPC.HOST,
port: config.SECOND_CORE_RPC.PORT,
user: config.SECOND_CORE_RPC.USERNAME,
pass: config.SECOND_CORE_RPC.PASSWORD,
timeout: config.SECOND_CORE_RPC.TIMEOUT,
+ cookie: config.SECOND_CORE_RPC.COOKIE ? config.SECOND_CORE_RPC.COOKIE_PATH || defaultCookiePath : undefined,
};
export default new bitcoin.Client(nodeRpcCredentials);
diff --git a/backend/src/config.ts b/backend/src/config.ts
index ff5ea4f9f..b9a3f366a 100644
--- a/backend/src/config.ts
+++ b/backend/src/config.ts
@@ -70,6 +70,8 @@ interface IConfig {
USERNAME: string;
PASSWORD: string;
TIMEOUT: number;
+ COOKIE: boolean;
+ COOKIE_PATH: string;
};
SECOND_CORE_RPC: {
HOST: string;
@@ -77,6 +79,8 @@ interface IConfig {
USERNAME: string;
PASSWORD: string;
TIMEOUT: number;
+ COOKIE: boolean;
+ COOKIE_PATH: string;
};
DATABASE: {
ENABLED: boolean;
@@ -180,6 +184,8 @@ const defaults: IConfig = {
'USERNAME': 'mempool',
'PASSWORD': 'mempool',
'TIMEOUT': 60000,
+ 'COOKIE': false,
+ 'COOKIE_PATH': '' // default value depends on network, see src/api/bitcoin/bitcoin-client
},
'SECOND_CORE_RPC': {
'HOST': '127.0.0.1',
@@ -187,6 +193,8 @@ const defaults: IConfig = {
'USERNAME': 'mempool',
'PASSWORD': 'mempool',
'TIMEOUT': 60000,
+ 'COOKIE': false,
+ 'COOKIE_PATH': ''
},
'DATABASE': {
'ENABLED': true,
diff --git a/backend/src/rpc-api/jsonrpc.ts b/backend/src/rpc-api/jsonrpc.ts
index 4f7a38baa..0bcbdc16c 100644
--- a/backend/src/rpc-api/jsonrpc.ts
+++ b/backend/src/rpc-api/jsonrpc.ts
@@ -1,5 +1,6 @@
var http = require('http')
var https = require('https')
+import { readFileSync } from 'fs';
var JsonRPC = function (opts) {
// @ts-ignore
@@ -55,7 +56,13 @@ JsonRPC.prototype.call = function (method, params) {
}
// use HTTP auth if user and password set
- if (this.opts.user && this.opts.pass) {
+ if (this.opts.cookie) {
+ if (!this.cachedCookie) {
+ this.cachedCookie = readFileSync(this.opts.cookie).toString();
+ }
+ // @ts-ignore
+ requestOptions.auth = this.cachedCookie;
+ } else if (this.opts.user && this.opts.pass) {
// @ts-ignore
requestOptions.auth = this.opts.user + ':' + this.opts.pass
}
@@ -93,7 +100,7 @@ JsonRPC.prototype.call = function (method, params) {
reject(err)
})
- request.on('response', function (response) {
+ request.on('response', (response) => {
clearTimeout(reqTimeout)
// We need to buffer the response chunks in a nonblocking way.
@@ -104,7 +111,7 @@ JsonRPC.prototype.call = function (method, params) {
// When all the responses are finished, we decode the JSON and
// depending on whether it's got a result or an error, we call
// emitSuccess or emitError on the promise.
- response.on('end', function () {
+ response.on('end', () => {
var err
if (cbCalled) return
@@ -113,6 +120,14 @@ JsonRPC.prototype.call = function (method, params) {
try {
var decoded = JSON.parse(buffer)
} catch (e) {
+ // if we authenticated using a cookie and it failed, read the cookie file again
+ if (
+ response.statusCode === 401 /* Unauthorized */ &&
+ this.opts.cookie
+ ) {
+ this.cachedCookie = undefined;
+ }
+
if (response.statusCode !== 200) {
err = new Error('Invalid params, response status code: ' + response.statusCode)
err.code = -32602
diff --git a/docker/README.md b/docker/README.md
index b669b37c8..74b9be0d9 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -162,7 +162,9 @@ Corresponding `docker-compose.yml` overrides:
"PORT": 8332,
"USERNAME": "mempool",
"PASSWORD": "mempool",
- "TIMEOUT": 60000
+ "TIMEOUT": 60000,
+ "COOKIE": "false",
+ "COOKIE_PATH": ""
},
```
@@ -174,7 +176,9 @@ Corresponding `docker-compose.yml` overrides:
CORE_RPC_PORT: ""
CORE_RPC_USERNAME: ""
CORE_RPC_PASSWORD: ""
- CORE_RPC_TIMEOUT: 60000
+ CORE_RPC_TIMEOUT: 60000,
+ CORE_RPC_COOKIE: ""
+ CORE_RPC_COOKIE_PATH: ""
...
```
@@ -229,7 +233,9 @@ Corresponding `docker-compose.yml` overrides:
"PORT": 8332,
"USERNAME": "mempool",
"PASSWORD": "mempool",
- "TIMEOUT": 60000
+ "TIMEOUT": 60000,
+ "COOKIE": "false",
+ "COOKIE_PATH": ""
},
```
@@ -241,7 +247,9 @@ Corresponding `docker-compose.yml` overrides:
SECOND_CORE_RPC_PORT: ""
SECOND_CORE_RPC_USERNAME: ""
SECOND_CORE_RPC_PASSWORD: ""
- SECOND_CORE_RPC_TIMEOUT: ""
+ SECOND_CORE_RPC_TIMEOUT: "",
+ SECOND_CORE_RPC_COOKIE: ""
+ SECOND_CORE_RPC_COOKIE_PATH: ""
...
```
diff --git a/docker/backend/mempool-config.json b/docker/backend/mempool-config.json
index 06ed51f41..7a1db21b4 100644
--- a/docker/backend/mempool-config.json
+++ b/docker/backend/mempool-config.json
@@ -36,7 +36,9 @@
"PORT": __CORE_RPC_PORT__,
"USERNAME": "__CORE_RPC_USERNAME__",
"PASSWORD": "__CORE_RPC_PASSWORD__",
- "TIMEOUT": __CORE_RPC_TIMEOUT__
+ "TIMEOUT": __CORE_RPC_TIMEOUT__,
+ "COOKIE": __CORE_RPC_COOKIE__,
+ "COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
},
"ELECTRUM": {
"HOST": "__ELECTRUM_HOST__",
@@ -53,7 +55,9 @@
"PORT": __SECOND_CORE_RPC_PORT__,
"USERNAME": "__SECOND_CORE_RPC_USERNAME__",
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__",
- "TIMEOUT": __SECOND_CORE_RPC_TIMEOUT__
+ "TIMEOUT": __SECOND_CORE_RPC_TIMEOUT__,
+ "COOKIE": __SECOND_CORE_RPC_COOKIE__,
+ "COOKIE_PATH": "__SECOND_CORE_RPC_COOKIE_PATH__"
},
"DATABASE": {
"ENABLED": __DATABASE_ENABLED__,
diff --git a/docker/backend/start.sh b/docker/backend/start.sh
index cb1108a05..0aa8fde5c 100755
--- a/docker/backend/start.sh
+++ b/docker/backend/start.sh
@@ -38,6 +38,8 @@ __CORE_RPC_PORT__=${CORE_RPC_PORT:=8332}
__CORE_RPC_USERNAME__=${CORE_RPC_USERNAME:=mempool}
__CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool}
__CORE_RPC_TIMEOUT__=${CORE_RPC_TIMEOUT:=60000}
+__CORE_RPC_COOKIE__=${CORE_RPC_COOKIE:=false}
+__CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE:=""}
# ELECTRUM
__ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1}
@@ -55,6 +57,8 @@ __SECOND_CORE_RPC_PORT__=${SECOND_CORE_RPC_PORT:=8332}
__SECOND_CORE_RPC_USERNAME__=${SECOND_CORE_RPC_USERNAME:=mempool}
__SECOND_CORE_RPC_PASSWORD__=${SECOND_CORE_RPC_PASSWORD:=mempool}
__SECOND_CORE_RPC_TIMEOUT__=${SECOND_CORE_RPC_TIMEOUT:=60000}
+__SECOND_CORE_RPC_COOKIE__=${SECOND_CORE_RPC_COOKIE:=false}
+__SECOND_CORE_RPC_COOKIE_PATH__=${SECOND_CORE_RPC_COOKIE:=""}
# DATABASE
__DATABASE_ENABLED__=${DATABASE_ENABLED:=true}
@@ -165,6 +169,8 @@ sed -i "s!__CORE_RPC_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json
sed -i "s!__CORE_RPC_USERNAME__!${__CORE_RPC_USERNAME__}!g" mempool-config.json
sed -i "s!__CORE_RPC_PASSWORD__!${__CORE_RPC_PASSWORD__}!g" mempool-config.json
sed -i "s!__CORE_RPC_TIMEOUT__!${__CORE_RPC_TIMEOUT__}!g" mempool-config.json
+sed -i "s!__CORE_RPC_COOKIE__!${__CORE_RPC_COOKIE__}!g" mempool-config.json
+sed -i "s!__CORE_RPC_COOKIE_PATH__!${__CORE_RPC_COOKIE_PATH__}!g" mempool-config.json
sed -i "s!__ELECTRUM_HOST__!${__ELECTRUM_HOST__}!g" mempool-config.json
sed -i "s!__ELECTRUM_PORT__!${__ELECTRUM_PORT__}!g" mempool-config.json
@@ -179,6 +185,8 @@ sed -i "s!__SECOND_CORE_RPC_PORT__!${__SECOND_CORE_RPC_PORT__}!g" mempool-config
sed -i "s!__SECOND_CORE_RPC_USERNAME__!${__SECOND_CORE_RPC_USERNAME__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_PASSWORD__!${__SECOND_CORE_RPC_PASSWORD__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_TIMEOUT__!${__SECOND_CORE_RPC_TIMEOUT__}!g" mempool-config.json
+sed -i "s!__SECOND_CORE_RPC_COOKIE__!${__SECOND_CORE_RPC_COOKIE__}!g" mempool-config.json
+sed -i "s!__SECOND_CORE_RPC_COOKIE_PATH__!${__SECOND_CORE_RPC_COOKIE_PATH__}!g" mempool-config.json
sed -i "s!__DATABASE_ENABLED__!${__DATABASE_ENABLED__}!g" mempool-config.json
sed -i "s!__DATABASE_HOST__!${__DATABASE_HOST__}!g" mempool-config.json
From dc491a598405cfaa641dfbed011e5a5cd3812654 Mon Sep 17 00:00:00 2001
From: Antoni Spaanderman <56turtle56@gmail.com>
Date: Mon, 27 Mar 2023 21:28:45 +0200
Subject: [PATCH 004/639] change default cookie path to /bitcoin/.cookie
---
backend/src/__tests__/config.test.ts | 4 ++--
backend/src/api/bitcoin/bitcoin-client.ts | 4 +---
backend/src/api/bitcoin/bitcoin-second-client.ts | 4 +---
backend/src/config.ts | 4 ++--
4 files changed, 6 insertions(+), 10 deletions(-)
diff --git a/backend/src/__tests__/config.test.ts b/backend/src/__tests__/config.test.ts
index f4cf719c6..d1d8222bd 100644
--- a/backend/src/__tests__/config.test.ts
+++ b/backend/src/__tests__/config.test.ts
@@ -56,7 +56,7 @@ describe('Mempool Backend Config', () => {
PASSWORD: 'mempool',
TIMEOUT: 60000,
COOKIE: false,
- COOKIE_PATH: ''
+ COOKIE_PATH: '/bitcoin/.cookie'
});
expect(config.SECOND_CORE_RPC).toStrictEqual({
@@ -66,7 +66,7 @@ describe('Mempool Backend Config', () => {
PASSWORD: 'mempool',
TIMEOUT: 60000,
COOKIE: false,
- COOKIE_PATH: ''
+ COOKIE_PATH: '/bitcoin/.cookie'
});
expect(config.DATABASE).toStrictEqual({
diff --git a/backend/src/api/bitcoin/bitcoin-client.ts b/backend/src/api/bitcoin/bitcoin-client.ts
index f0dab4441..e8b30a888 100644
--- a/backend/src/api/bitcoin/bitcoin-client.ts
+++ b/backend/src/api/bitcoin/bitcoin-client.ts
@@ -2,15 +2,13 @@ import config from '../../config';
const bitcoin = require('../../rpc-api/index');
import { BitcoinRpcCredentials } from './bitcoin-api-abstract-factory';
-export const defaultCookiePath = `${process.env.HOME}/.bitcoin/${{mainnet:'',testnet:'testnet3/',signet:'signet/'}[config.MEMPOOL.NETWORK]}.cookie`;
-
const nodeRpcCredentials: BitcoinRpcCredentials = {
host: config.CORE_RPC.HOST,
port: config.CORE_RPC.PORT,
user: config.CORE_RPC.USERNAME,
pass: config.CORE_RPC.PASSWORD,
timeout: config.CORE_RPC.TIMEOUT,
- cookie: config.CORE_RPC.COOKIE ? config.CORE_RPC.COOKIE_PATH || defaultCookiePath : undefined,
+ cookie: config.CORE_RPC.COOKIE ? config.CORE_RPC.COOKIE_PATH : undefined,
};
export default new bitcoin.Client(nodeRpcCredentials);
diff --git a/backend/src/api/bitcoin/bitcoin-second-client.ts b/backend/src/api/bitcoin/bitcoin-second-client.ts
index 85d05556e..6ae9cefb0 100644
--- a/backend/src/api/bitcoin/bitcoin-second-client.ts
+++ b/backend/src/api/bitcoin/bitcoin-second-client.ts
@@ -2,15 +2,13 @@ import config from '../../config';
const bitcoin = require('../../rpc-api/index');
import { BitcoinRpcCredentials } from './bitcoin-api-abstract-factory';
-import { defaultCookiePath } from './bitcoin-client';
-
const nodeRpcCredentials: BitcoinRpcCredentials = {
host: config.SECOND_CORE_RPC.HOST,
port: config.SECOND_CORE_RPC.PORT,
user: config.SECOND_CORE_RPC.USERNAME,
pass: config.SECOND_CORE_RPC.PASSWORD,
timeout: config.SECOND_CORE_RPC.TIMEOUT,
- cookie: config.SECOND_CORE_RPC.COOKIE ? config.SECOND_CORE_RPC.COOKIE_PATH || defaultCookiePath : undefined,
+ cookie: config.SECOND_CORE_RPC.COOKIE ? config.SECOND_CORE_RPC.COOKIE_PATH : undefined,
};
export default new bitcoin.Client(nodeRpcCredentials);
diff --git a/backend/src/config.ts b/backend/src/config.ts
index b9a3f366a..eb1b0af21 100644
--- a/backend/src/config.ts
+++ b/backend/src/config.ts
@@ -185,7 +185,7 @@ const defaults: IConfig = {
'PASSWORD': 'mempool',
'TIMEOUT': 60000,
'COOKIE': false,
- 'COOKIE_PATH': '' // default value depends on network, see src/api/bitcoin/bitcoin-client
+ 'COOKIE_PATH': '/bitcoin/.cookie'
},
'SECOND_CORE_RPC': {
'HOST': '127.0.0.1',
@@ -194,7 +194,7 @@ const defaults: IConfig = {
'PASSWORD': 'mempool',
'TIMEOUT': 60000,
'COOKIE': false,
- 'COOKIE_PATH': ''
+ 'COOKIE_PATH': '/bitcoin/.cookie'
},
'DATABASE': {
'ENABLED': true,
From 0d69ad43ab26c7d2150e4b8460e762b7e06241e0 Mon Sep 17 00:00:00 2001
From: Antoni Spaanderman <56turtle56@gmail.com>
Date: Sun, 18 Jun 2023 20:34:56 +0200
Subject: [PATCH 005/639] fix config issues
---
.../src/__fixtures__/mempool-config.template.json | 4 ++--
docker/README.md | 12 ++++++------
docker/backend/mempool-config.json | 2 +-
docker/backend/start.sh | 4 ++--
4 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/backend/src/__fixtures__/mempool-config.template.json b/backend/src/__fixtures__/mempool-config.template.json
index 1d5c7135a..cdf188b88 100644
--- a/backend/src/__fixtures__/mempool-config.template.json
+++ b/backend/src/__fixtures__/mempool-config.template.json
@@ -37,7 +37,7 @@
"USERNAME": "__CORE_RPC_USERNAME__",
"PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": 1000,
- "COOKIE": "__CORE_RPC_COOKIE__",
+ "COOKIE": false,
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
},
"ELECTRUM": {
@@ -56,7 +56,7 @@
"USERNAME": "__SECOND_CORE_RPC_USERNAME__",
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__",
"TIMEOUT": 2000,
- "COOKIE": "__SECOND_CORE_RPC_COOKIE__",
+ "COOKIE": false,
"COOKIE_PATH": "__SECOND_CORE_RPC_COOKIE_PATH__"
},
"DATABASE": {
diff --git a/docker/README.md b/docker/README.md
index 74b9be0d9..997a330b4 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -163,7 +163,7 @@ Corresponding `docker-compose.yml` overrides:
"USERNAME": "mempool",
"PASSWORD": "mempool",
"TIMEOUT": 60000,
- "COOKIE": "false",
+ "COOKIE": false,
"COOKIE_PATH": ""
},
```
@@ -176,8 +176,8 @@ Corresponding `docker-compose.yml` overrides:
CORE_RPC_PORT: ""
CORE_RPC_USERNAME: ""
CORE_RPC_PASSWORD: ""
- CORE_RPC_TIMEOUT: 60000,
- CORE_RPC_COOKIE: ""
+ CORE_RPC_TIMEOUT: 60000
+ CORE_RPC_COOKIE: false
CORE_RPC_COOKIE_PATH: ""
...
```
@@ -234,7 +234,7 @@ Corresponding `docker-compose.yml` overrides:
"USERNAME": "mempool",
"PASSWORD": "mempool",
"TIMEOUT": 60000,
- "COOKIE": "false",
+ "COOKIE": false,
"COOKIE_PATH": ""
},
```
@@ -247,8 +247,8 @@ Corresponding `docker-compose.yml` overrides:
SECOND_CORE_RPC_PORT: ""
SECOND_CORE_RPC_USERNAME: ""
SECOND_CORE_RPC_PASSWORD: ""
- SECOND_CORE_RPC_TIMEOUT: "",
- SECOND_CORE_RPC_COOKIE: ""
+ SECOND_CORE_RPC_TIMEOUT: ""
+ SECOND_CORE_RPC_COOKIE: false
SECOND_CORE_RPC_COOKIE_PATH: ""
...
```
diff --git a/docker/backend/mempool-config.json b/docker/backend/mempool-config.json
index 7a1db21b4..a91a52d6b 100644
--- a/docker/backend/mempool-config.json
+++ b/docker/backend/mempool-config.json
@@ -129,4 +129,4 @@
"GEOLITE2_ASN": "__MAXMIND_GEOLITE2_ASN__",
"GEOIP2_ISP": "__MAXMIND_GEOIP2_ISP__"
}
-}
\ No newline at end of file
+}
diff --git a/docker/backend/start.sh b/docker/backend/start.sh
index 0aa8fde5c..1dce5e811 100755
--- a/docker/backend/start.sh
+++ b/docker/backend/start.sh
@@ -39,7 +39,7 @@ __CORE_RPC_USERNAME__=${CORE_RPC_USERNAME:=mempool}
__CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool}
__CORE_RPC_TIMEOUT__=${CORE_RPC_TIMEOUT:=60000}
__CORE_RPC_COOKIE__=${CORE_RPC_COOKIE:=false}
-__CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE:=""}
+__CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE_PATH:=""}
# ELECTRUM
__ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1}
@@ -58,7 +58,7 @@ __SECOND_CORE_RPC_USERNAME__=${SECOND_CORE_RPC_USERNAME:=mempool}
__SECOND_CORE_RPC_PASSWORD__=${SECOND_CORE_RPC_PASSWORD:=mempool}
__SECOND_CORE_RPC_TIMEOUT__=${SECOND_CORE_RPC_TIMEOUT:=60000}
__SECOND_CORE_RPC_COOKIE__=${SECOND_CORE_RPC_COOKIE:=false}
-__SECOND_CORE_RPC_COOKIE_PATH__=${SECOND_CORE_RPC_COOKIE:=""}
+__SECOND_CORE_RPC_COOKIE_PATH__=${SECOND_CORE_RPC_COOKIE_PATH:=""}
# DATABASE
__DATABASE_ENABLED__=${DATABASE_ENABLED:=true}
From 1339b98281b235b6e4a5a15db041df835b37a686 Mon Sep 17 00:00:00 2001
From: junderw
Date: Sat, 27 Aug 2022 16:35:20 +0900
Subject: [PATCH 006/639] Feature: Readable RegExp constructor
---
frontend/src/app/shared/common.utils.ts | 69 ++++++++++++++++++++++++-
1 file changed, 68 insertions(+), 1 deletion(-)
diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts
index 7d206f4b5..6cbe35386 100644
--- a/frontend/src/app/shared/common.utils.ts
+++ b/frontend/src/app/shared/common.utils.ts
@@ -119,6 +119,7 @@ export function convertRegion(input, to: 'name' | 'abbreviated'): string {
}
}
+
export function haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
const rlat1 = lat1 * Math.PI / 180;
const rlon1 = lon1 * Math.PI / 180;
@@ -135,4 +136,70 @@ export function haversineDistance(lat1: number, lon1: number, lat2: number, lon2
export function kmToMiles(km: number): number {
return km * 0.62137119;
-}
\ No newline at end of file
+}
+
+// all base58 characters
+const BASE58_CHARS = '[a-km-zA-HJ-NP-Z1-9]';
+// all bech32 characters (after the separator)
+const BECH32_CHARS = '[ac-hj-np-z02-9]';
+// All characters usable in bech32 human readable portion (before the 1 separator)
+// Note: Technically the spec says "all US ASCII characters" but in practice only alphabet is used.
+// Note: If HRP contains the separator (1) then the separator is "the last instance of separator"
+const BECH32_HRP_CHARS = '[a-zA-Z0-9]';
+// Hex characters
+const HEX_CHARS = '[a-fA-F0-9]';
+// A regex to say "A single 0 OR any number with no leading zeroes"
+// (?: // Start a non-capturing group
+// 0 // A single 0
+// | // OR
+// [1-9][0-9]* // Any succession of numbers starting with 1-9
+// ) // End the non-capturing group.
+const ZERO_INDEX_NUMBER_CHARS = '(?:0|[1-9][0-9]*)';
+export type RegexType = 'address' | 'blockhash' | 'transaction' | 'blockheight';
+export type Network = 'testnet' | 'signet' | 'liquid' | 'bisq' | 'mainnet';
+export function getRegex(type: RegexType, network: Network): RegExp {
+ let regex = '^'; // ^ = Start of string
+ switch (type) {
+ // Match a block height number
+ // [Testing Order]: any order is fine
+ case 'blockheight':
+ regex += ZERO_INDEX_NUMBER_CHARS; // block height is a 0 indexed number
+ break;
+ // Match a 32 byte block hash in hex. Assumes at least 32 bits of difficulty.
+ // [Testing Order]: Must always be tested before 'transaction'
+ case 'blockhash':
+ regex += '0{8}'; // Starts with exactly 8 zeroes in a row
+ regex += `${HEX_CHARS}{56}`; // Continues with exactly 56 hex letters/numbers
+ break;
+ // Match a 32 byte tx hash in hex. Contains optional output index specifier.
+ // [Testing Order]: Must always be tested after 'blockhash'
+ case 'transaction':
+ regex += `${HEX_CHARS}{64}`; // Exactly 64 hex letters/numbers
+ regex += '(?:'; // Start a non-capturing group
+ regex += ':'; // 1 instances of the symbol ":"
+ regex += ZERO_INDEX_NUMBER_CHARS; // A zero indexed number
+ regex += ')?'; // End the non-capturing group. This group appears 0 or 1 times
+ break;
+ case 'address':
+ // TODO
+ switch (network) {
+ case 'mainnet':
+ break;
+ case 'testnet':
+ break;
+ case 'signet':
+ break;
+ case 'liquid':
+ break;
+ case 'bisq':
+ break;
+ default:
+ throw new Error('Invalid Network (Unreachable error in TypeScript)');
+ }
+ break;
+ default:
+ throw new Error('Invalid RegexType (Unreachable error in TypeScript)');
+ }
+ regex += '$'; // $ = End of string
+ return new RegExp(regex);
+}
From c0d3f295eec2f483e8006b6f06de6fd8b3650a5b Mon Sep 17 00:00:00 2001
From: junderw
Date: Sun, 28 Aug 2022 00:07:13 +0900
Subject: [PATCH 007/639] Finished Regex portion
---
.../search-form/search-form.component.ts | 25 ++-
frontend/src/app/shared/common.utils.ts | 191 ++++++++++++++----
2 files changed, 178 insertions(+), 38 deletions(-)
diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts
index ab42fe1f7..8031195f0 100644
--- a/frontend/src/app/components/search-form/search-form.component.ts
+++ b/frontend/src/app/components/search-form/search-form.component.ts
@@ -9,6 +9,7 @@ import { ElectrsApiService } from '../../services/electrs-api.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { ApiService } from '../../services/api.service';
import { SearchResultsComponent } from './search-results/search-results.component';
+import { ADDRESS_REGEXES, getRegex } from '../../shared/common.utils';
@Component({
selector: 'app-search-form',
@@ -38,6 +39,7 @@ export class SearchFormComponent implements OnInit {
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
regexBlockheight = /^[0-9]{1,9}$/;
+
focus$ = new Subject();
click$ = new Subject();
@@ -58,8 +60,13 @@ export class SearchFormComponent implements OnInit {
private elementRef: ElementRef,
) { }
- ngOnInit(): void {
- this.stateService.networkChanged$.subscribe((network) => this.network = network);
+
+ ngOnInit() {
+ this.stateService.networkChanged$.subscribe((network) => {
+ this.network = network;
+ // TODO: Eventually change network type here from string to enum of consts
+ this.regexAddress = getRegex('address', network as any);
+ });
this.searchForm = this.formBuilder.group({
searchText: ['', Validators.required],
@@ -203,6 +210,20 @@ export class SearchFormComponent implements OnInit {
this.isSearching = true;
if (!this.regexTransaction.test(searchText) && this.regexAddress.test(searchText)) {
this.navigate('/address/', searchText);
+ } else if (
+ // If the search text matches any other network besides this one
+ ADDRESS_REGEXES
+ .filter(([, network]) => network !== this.network)
+ .some(([regex]) => regex.test(searchText))
+ ) {
+ // Gather all network matches as string[]
+ const networks = ADDRESS_REGEXES.filter(([regex, network]) =>
+ network !== this.network &&
+ regex.test(searchText)
+ ).map(([, network]) => network);
+ // ###############################################
+ // TODO: Create the search items for the drop down
+ // ###############################################
} else if (this.regexBlockhash.test(searchText) || this.regexBlockheight.test(searchText)) {
this.navigate('/block/', searchText);
} else if (this.regexTransaction.test(searchText)) {
diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts
index 6cbe35386..bbc9143c0 100644
--- a/frontend/src/app/shared/common.utils.ts
+++ b/frontend/src/app/shared/common.utils.ts
@@ -139,67 +139,186 @@ export function kmToMiles(km: number): number {
}
// all base58 characters
-const BASE58_CHARS = '[a-km-zA-HJ-NP-Z1-9]';
+const BASE58_CHARS = `[a-km-zA-HJ-NP-Z1-9]`;
+
// all bech32 characters (after the separator)
-const BECH32_CHARS = '[ac-hj-np-z02-9]';
-// All characters usable in bech32 human readable portion (before the 1 separator)
-// Note: Technically the spec says "all US ASCII characters" but in practice only alphabet is used.
-// Note: If HRP contains the separator (1) then the separator is "the last instance of separator"
-const BECH32_HRP_CHARS = '[a-zA-Z0-9]';
+const BECH32_CHARS_LW = `[ac-hj-np-z02-9]`;
+const BECH32_CHARS_UP = `[AC-HJ-NP-Z02-9]`;
+
// Hex characters
-const HEX_CHARS = '[a-fA-F0-9]';
+const HEX_CHARS = `[a-fA-F0-9]`;
+
// A regex to say "A single 0 OR any number with no leading zeroes"
-// (?: // Start a non-capturing group
-// 0 // A single 0
-// | // OR
-// [1-9][0-9]* // Any succession of numbers starting with 1-9
-// ) // End the non-capturing group.
-const ZERO_INDEX_NUMBER_CHARS = '(?:0|[1-9][0-9]*)';
-export type RegexType = 'address' | 'blockhash' | 'transaction' | 'blockheight';
-export type Network = 'testnet' | 'signet' | 'liquid' | 'bisq' | 'mainnet';
-export function getRegex(type: RegexType, network: Network): RegExp {
- let regex = '^'; // ^ = Start of string
+// (?: // Start a non-capturing group
+// 0 // A single 0
+// | // OR
+// [1-9]\d* // Any succession of numbers starting with 1-9
+// ) // End the non-capturing group.
+const ZERO_INDEX_NUMBER_CHARS = `(?:0|[1-9]\d*)`;
+
+// Formatting of the address regex is for readability,
+// We should ignore formatting it with automated formatting tools like prettier.
+//
+// prettier-ignore
+const ADDRESS_CHARS = {
+ mainnet: {
+ base58: `[13]` // Starts with a single 1 or 3
+ + BASE58_CHARS
+ + `{26,33}`, // Repeat the previous char 26-33 times.
+ // Version byte 0x00 (P2PKH) can be as short as 27 characters, up to 34 length
+ // P2SH must be 34 length
+ bech32: `(?:`
+ + `bc1` // Starts with bc1
+ + BECH32_CHARS_LW
+ + `{6,100}` // As per bech32, 6 char checksum is minimum
+ + `|`
+ + `BC1` // All upper case version
+ + BECH32_CHARS_UP
+ + `{6,100}`
+ + `)`,
+ },
+ testnet: {
+ base58: `[mn2]` // Starts with a single m, n, or 2 (P2PKH is m or n, 2 is P2SH)
+ + BASE58_CHARS
+ + `{33,34}`, // m|n is 34 length, 2 is 35 length (We match the first letter separately)
+ bech32: `(?:`
+ + `tb1` // Starts with bc1
+ + BECH32_CHARS_LW
+ + `{6,100}` // As per bech32, 6 char checksum is minimum
+ + `|`
+ + `TB1` // All upper case version
+ + BECH32_CHARS_UP
+ + `{6,100}`
+ + `)`,
+ },
+ signet: {
+ base58: `[mn2]`
+ + BASE58_CHARS
+ + `{33,34}`,
+ bech32: `(?:`
+ + `tb1` // Starts with tb1
+ + BECH32_CHARS_LW
+ + `{6,100}`
+ + `|`
+ + `TB1` // All upper case version
+ + BECH32_CHARS_UP
+ + `{6,100}`
+ + `)`,
+ },
+ liquid: {
+ base58: `[GHPQ]` // G|H is P2PKH, P|Q is P2SH
+ + BASE58_CHARS
+ + `{33}`, // All min-max lengths are 34
+ bech32: `(?:`
+ + `(?:` // bech32 liquid starts with ex or lq
+ + `ex`
+ + `|`
+ + `lq`
+ + `)`
+ + BECH32_CHARS_LW // blech32 and bech32 are the same alphabet and protocol, different checksums.
+ + `{6,100}`
+ + `|`
+ + `(?:` // Same as above but all upper case
+ + `EX`
+ + `|`
+ + `LQ`
+ + `)`
+ + BECH32_CHARS_UP
+ + `{6,100}`
+ + `)`,
+ },
+ bisq: {
+ base58: `B1` // bisq base58 addrs start with B1
+ + BASE58_CHARS
+ + `{33}`, // always length 35
+ bech32: `(?:`
+ + `bbc1` // Starts with bbc1
+ + BECH32_CHARS_LW
+ + `{6,100}`
+ + `|`
+ + `BBC1` // All upper case version
+ + BECH32_CHARS_UP
+ + `{6,100}`
+ + `)`,
+ },
+}
+type RegexTypeNoAddr = `blockhash` | `transaction` | `blockheight`;
+export type RegexType = `address` | RegexTypeNoAddr;
+
+export const NETWORKS = [`testnet`, `signet`, `liquid`, `bisq`, `mainnet`] as const;
+export type Network = typeof NETWORKS[number]; // Turn const array into union type
+
+export const ADDRESS_REGEXES: [RegExp, string][] = NETWORKS
+ .map(network => [getRegex('address', network), network])
+
+export function getRegex(type: RegexTypeNoAddr): RegExp;
+export function getRegex(type: 'address', network: Network): RegExp;
+export function getRegex(type: RegexType, network?: Network): RegExp {
+ let regex = `^`; // ^ = Start of string
switch (type) {
// Match a block height number
// [Testing Order]: any order is fine
- case 'blockheight':
+ case `blockheight`:
regex += ZERO_INDEX_NUMBER_CHARS; // block height is a 0 indexed number
break;
// Match a 32 byte block hash in hex. Assumes at least 32 bits of difficulty.
- // [Testing Order]: Must always be tested before 'transaction'
- case 'blockhash':
- regex += '0{8}'; // Starts with exactly 8 zeroes in a row
+ // [Testing Order]: Must always be tested before `transaction`
+ case `blockhash`:
+ regex += `0{8}`; // Starts with exactly 8 zeroes in a row
regex += `${HEX_CHARS}{56}`; // Continues with exactly 56 hex letters/numbers
break;
// Match a 32 byte tx hash in hex. Contains optional output index specifier.
- // [Testing Order]: Must always be tested after 'blockhash'
- case 'transaction':
+ // [Testing Order]: Must always be tested after `blockhash`
+ case `transaction`:
regex += `${HEX_CHARS}{64}`; // Exactly 64 hex letters/numbers
- regex += '(?:'; // Start a non-capturing group
- regex += ':'; // 1 instances of the symbol ":"
+ regex += `(?:`; // Start a non-capturing group
+ regex += `:`; // 1 instances of the symbol ":"
regex += ZERO_INDEX_NUMBER_CHARS; // A zero indexed number
- regex += ')?'; // End the non-capturing group. This group appears 0 or 1 times
+ regex += `)?`; // End the non-capturing group. This group appears 0 or 1 times
break;
- case 'address':
- // TODO
+ // Match any one of the many address types
+ // [Testing Order]: While possible that a bech32 address happens to be 64 hex
+ // characters in the future (current lengths are not 64), it is highly unlikely
+ // Order therefore, does not matter.
+ case `address`:
+ if (!network) {
+ throw new Error(`Must pass network when type is address`);
+ }
+ regex += `(?:`; // Start a non-capturing group (each network has multiple options)
switch (network) {
- case 'mainnet':
+ case `mainnet`:
+ regex += ADDRESS_CHARS.mainnet.base58;
+ regex += `|`; // OR
+ regex += ADDRESS_CHARS.mainnet.bech32;
break;
- case 'testnet':
+ case `testnet`:
+ regex += ADDRESS_CHARS.testnet.base58;
+ regex += `|`; // OR
+ regex += ADDRESS_CHARS.testnet.bech32;
break;
- case 'signet':
+ case `signet`:
+ regex += ADDRESS_CHARS.signet.base58;
+ regex += `|`; // OR
+ regex += ADDRESS_CHARS.signet.bech32;
break;
- case 'liquid':
+ case `liquid`:
+ regex += ADDRESS_CHARS.liquid.base58;
+ regex += `|`; // OR
+ regex += ADDRESS_CHARS.liquid.bech32;
break;
- case 'bisq':
+ case `bisq`:
+ regex += ADDRESS_CHARS.bisq.base58;
+ regex += `|`; // OR
+ regex += ADDRESS_CHARS.bisq.bech32;
break;
default:
- throw new Error('Invalid Network (Unreachable error in TypeScript)');
+ throw new Error(`Invalid Network ${network} (Unreachable error in TypeScript)`);
}
+ regex += `)`; // End the non-capturing group
break;
default:
- throw new Error('Invalid RegexType (Unreachable error in TypeScript)');
+ throw new Error(`Invalid RegexType ${type} (Unreachable error in TypeScript)`);
}
- regex += '$'; // $ = End of string
+ regex += `$`; // $ = End of string
return new RegExp(regex);
}
From 0a51b752e62d82b6986c8177cc63a48991383fcb Mon Sep 17 00:00:00 2001
From: junderw
Date: Sun, 28 Aug 2022 16:07:46 +0900
Subject: [PATCH 008/639] Improve types and add liquidtestnet for regex
---
frontend/src/app/shared/common.utils.ts | 38 +++++++++++++++++++++++--
1 file changed, 35 insertions(+), 3 deletions(-)
diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts
index bbc9143c0..288fc0362 100644
--- a/frontend/src/app/shared/common.utils.ts
+++ b/frontend/src/app/shared/common.utils.ts
@@ -160,7 +160,12 @@ const ZERO_INDEX_NUMBER_CHARS = `(?:0|[1-9]\d*)`;
// We should ignore formatting it with automated formatting tools like prettier.
//
// prettier-ignore
-const ADDRESS_CHARS = {
+const ADDRESS_CHARS: {
+ [k in Network]: {
+ base58: string;
+ bech32: string;
+ };
+} = {
mainnet: {
base58: `[13]` // Starts with a single 1 or 3
+ BASE58_CHARS
@@ -227,6 +232,28 @@ const ADDRESS_CHARS = {
+ `{6,100}`
+ `)`,
},
+ liquidtestnet: {
+ base58: `[89]` // ???(TODO: find version) is P2PKH, 8|9 is P2SH
+ + BASE58_CHARS
+ + `{33}`, // P2PKH is ???(TODO: find size), P2SH is 34
+ bech32: `(?:`
+ + `(?:` // bech32 liquid testnet starts with tex or tlq
+ + `tex` // TODO: Why does mempool use this and not ert|el like in the elements source?
+ + `|`
+ + `tlq` // TODO: does this exist?
+ + `)`
+ + BECH32_CHARS_LW // blech32 and bech32 are the same alphabet and protocol, different checksums.
+ + `{6,100}`
+ + `|`
+ + `(?:` // Same as above but all upper case
+ + `TEX`
+ + `|`
+ + `TLQ`
+ + `)`
+ + BECH32_CHARS_UP
+ + `{6,100}`
+ + `)`,
+ },
bisq: {
base58: `B1` // bisq base58 addrs start with B1
+ BASE58_CHARS
@@ -245,10 +272,10 @@ const ADDRESS_CHARS = {
type RegexTypeNoAddr = `blockhash` | `transaction` | `blockheight`;
export type RegexType = `address` | RegexTypeNoAddr;
-export const NETWORKS = [`testnet`, `signet`, `liquid`, `bisq`, `mainnet`] as const;
+export const NETWORKS = [`testnet`, `signet`, `liquid`, `liquidtestnet`, `bisq`, `mainnet`] as const;
export type Network = typeof NETWORKS[number]; // Turn const array into union type
-export const ADDRESS_REGEXES: [RegExp, string][] = NETWORKS
+export const ADDRESS_REGEXES: [RegExp, Network][] = NETWORKS
.map(network => [getRegex('address', network), network])
export function getRegex(type: RegexTypeNoAddr): RegExp;
@@ -306,6 +333,11 @@ export function getRegex(type: RegexType, network?: Network): RegExp {
regex += `|`; // OR
regex += ADDRESS_CHARS.liquid.bech32;
break;
+ case `liquidtestnet`:
+ regex += ADDRESS_CHARS.liquidtestnet.base58;
+ regex += `|`; // OR
+ regex += ADDRESS_CHARS.liquidtestnet.bech32;
+ break;
case `bisq`:
regex += ADDRESS_CHARS.bisq.base58;
regex += `|`; // OR
From 3d900a38497d7d6b588abba8d06c2af2c8c5d663 Mon Sep 17 00:00:00 2001
From: junderw
Date: Sun, 28 Aug 2022 16:28:42 +0900
Subject: [PATCH 009/639] Fix: Prevent regex clash with channel IDs
---
frontend/src/app/shared/common.utils.ts | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts
index 288fc0362..8b914beda 100644
--- a/frontend/src/app/shared/common.utils.ts
+++ b/frontend/src/app/shared/common.utils.ts
@@ -149,12 +149,13 @@ const BECH32_CHARS_UP = `[AC-HJ-NP-Z02-9]`;
const HEX_CHARS = `[a-fA-F0-9]`;
// A regex to say "A single 0 OR any number with no leading zeroes"
-// (?: // Start a non-capturing group
-// 0 // A single 0
-// | // OR
-// [1-9]\d* // Any succession of numbers starting with 1-9
-// ) // End the non-capturing group.
-const ZERO_INDEX_NUMBER_CHARS = `(?:0|[1-9]\d*)`;
+// Capped at 13 digits so as to not be confused with lightning channel IDs (which are around 17 digits)
+// (?: // Start a non-capturing group
+// 0 // A single 0
+// | // OR
+// [1-9][0-9]{0,12} // Any succession of numbers up to 13 digits starting with 1-9
+// ) // End the non-capturing group.
+const ZERO_INDEX_NUMBER_CHARS = `(?:0|[1-9][0-9]{0,12})`;
// Formatting of the address regex is for readability,
// We should ignore formatting it with automated formatting tools like prettier.
From d825143b3562c4ab493f56ae67a4eadcfde7a469 Mon Sep 17 00:00:00 2001
From: junderw
Date: Sun, 4 Sep 2022 21:31:02 +0900
Subject: [PATCH 010/639] Search for full address in separate network if
matches
---
.../search-form/search-form.component.ts | 26 +-
frontend/src/app/shared/common.utils.ts | 1 -
.../pipes/relative-url/relative-url.pipe.ts | 4 +-
frontend/src/app/shared/regex.utils.ts | 224 ++++++++++++++++++
4 files changed, 235 insertions(+), 20 deletions(-)
create mode 100644 frontend/src/app/shared/regex.utils.ts
diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts
index 8031195f0..a9e31221a 100644
--- a/frontend/src/app/components/search-form/search-form.component.ts
+++ b/frontend/src/app/components/search-form/search-form.component.ts
@@ -9,7 +9,7 @@ import { ElectrsApiService } from '../../services/electrs-api.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { ApiService } from '../../services/api.service';
import { SearchResultsComponent } from './search-results/search-results.component';
-import { ADDRESS_REGEXES, getRegex } from '../../shared/common.utils';
+import { findOtherNetworks, getRegex } from '../../shared/regex.utils';
@Component({
selector: 'app-search-form',
@@ -208,22 +208,13 @@ export class SearchFormComponent implements OnInit {
const searchText = result || this.searchForm.value.searchText.trim();
if (searchText) {
this.isSearching = true;
+
+ const otherNetworks = findOtherNetworks(searchText, this.network as any);
if (!this.regexTransaction.test(searchText) && this.regexAddress.test(searchText)) {
this.navigate('/address/', searchText);
- } else if (
- // If the search text matches any other network besides this one
- ADDRESS_REGEXES
- .filter(([, network]) => network !== this.network)
- .some(([regex]) => regex.test(searchText))
- ) {
- // Gather all network matches as string[]
- const networks = ADDRESS_REGEXES.filter(([regex, network]) =>
- network !== this.network &&
- regex.test(searchText)
- ).map(([, network]) => network);
- // ###############################################
- // TODO: Create the search items for the drop down
- // ###############################################
+ } else if (otherNetworks.length > 0) {
+ // Change the network to the first match
+ this.navigate('/address/', searchText, undefined, otherNetworks[0]);
} else if (this.regexBlockhash.test(searchText) || this.regexBlockheight.test(searchText)) {
this.navigate('/block/', searchText);
} else if (this.regexTransaction.test(searchText)) {
@@ -252,8 +243,9 @@ export class SearchFormComponent implements OnInit {
}
}
- navigate(url: string, searchText: string, extras?: any): void {
- this.router.navigate([this.relativeUrlPipe.transform(url), searchText], extras);
+
+ navigate(url: string, searchText: string, extras?: any, swapNetwork?: string) {
+ this.router.navigate([this.relativeUrlPipe.transform(url, swapNetwork), searchText], extras);
this.searchTriggered.emit();
this.searchForm.setValue({
searchText: '',
diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts
index 8b914beda..e50ba13b7 100644
--- a/frontend/src/app/shared/common.utils.ts
+++ b/frontend/src/app/shared/common.utils.ts
@@ -119,7 +119,6 @@ export function convertRegion(input, to: 'name' | 'abbreviated'): string {
}
}
-
export function haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
const rlat1 = lat1 * Math.PI / 180;
const rlon1 = lon1 * Math.PI / 180;
diff --git a/frontend/src/app/shared/pipes/relative-url/relative-url.pipe.ts b/frontend/src/app/shared/pipes/relative-url/relative-url.pipe.ts
index d7fe612fe..83f5f20df 100644
--- a/frontend/src/app/shared/pipes/relative-url/relative-url.pipe.ts
+++ b/frontend/src/app/shared/pipes/relative-url/relative-url.pipe.ts
@@ -10,8 +10,8 @@ export class RelativeUrlPipe implements PipeTransform {
private stateService: StateService,
) { }
- transform(value: string): string {
- let network = this.stateService.network;
+ transform(value: string, swapNetwork?: string): string {
+ let network = swapNetwork || this.stateService.network;
if (this.stateService.env.BASE_MODULE === 'liquid' && network === 'liquidtestnet') {
network = 'testnet';
} else if (this.stateService.env.BASE_MODULE !== 'mempool') {
diff --git a/frontend/src/app/shared/regex.utils.ts b/frontend/src/app/shared/regex.utils.ts
new file mode 100644
index 000000000..bac256c8d
--- /dev/null
+++ b/frontend/src/app/shared/regex.utils.ts
@@ -0,0 +1,224 @@
+// all base58 characters
+const BASE58_CHARS = `[a-km-zA-HJ-NP-Z1-9]`;
+
+// all bech32 characters (after the separator)
+const BECH32_CHARS_LW = `[ac-hj-np-z02-9]`;
+const BECH32_CHARS_UP = `[AC-HJ-NP-Z02-9]`;
+
+// Hex characters
+const HEX_CHARS = `[a-fA-F0-9]`;
+
+// A regex to say "A single 0 OR any number with no leading zeroes"
+// Capped at 13 digits so as to not be confused with lightning channel IDs (which are around 17 digits)
+// (?: // Start a non-capturing group
+// 0 // A single 0
+// | // OR
+// [1-9][0-9]{0,12} // Any succession of numbers up to 13 digits starting with 1-9
+// ) // End the non-capturing group.
+const ZERO_INDEX_NUMBER_CHARS = `(?:0|[1-9][0-9]{0,12})`;
+
+// Formatting of the address regex is for readability,
+// We should ignore formatting it with automated formatting tools like prettier.
+//
+// prettier-ignore
+const ADDRESS_CHARS: {
+ [k in Network]: {
+ base58: string;
+ bech32: string;
+ };
+} = {
+ mainnet: {
+ base58: `[13]` // Starts with a single 1 or 3
+ + BASE58_CHARS
+ + `{26,33}`, // Repeat the previous char 26-33 times.
+ // Version byte 0x00 (P2PKH) can be as short as 27 characters, up to 34 length
+ // P2SH must be 34 length
+ bech32: `(?:`
+ + `bc1` // Starts with bc1
+ + BECH32_CHARS_LW
+ + `{6,100}` // As per bech32, 6 char checksum is minimum
+ + `|`
+ + `BC1` // All upper case version
+ + BECH32_CHARS_UP
+ + `{6,100}`
+ + `)`,
+ },
+ testnet: {
+ base58: `[mn2]` // Starts with a single m, n, or 2 (P2PKH is m or n, 2 is P2SH)
+ + BASE58_CHARS
+ + `{33,34}`, // m|n is 34 length, 2 is 35 length (We match the first letter separately)
+ bech32: `(?:`
+ + `tb1` // Starts with bc1
+ + BECH32_CHARS_LW
+ + `{6,100}` // As per bech32, 6 char checksum is minimum
+ + `|`
+ + `TB1` // All upper case version
+ + BECH32_CHARS_UP
+ + `{6,100}`
+ + `)`,
+ },
+ signet: {
+ base58: `[mn2]`
+ + BASE58_CHARS
+ + `{33,34}`,
+ bech32: `(?:`
+ + `tb1` // Starts with tb1
+ + BECH32_CHARS_LW
+ + `{6,100}`
+ + `|`
+ + `TB1` // All upper case version
+ + BECH32_CHARS_UP
+ + `{6,100}`
+ + `)`,
+ },
+ liquid: {
+ base58: `[GHPQ]` // G|H is P2PKH, P|Q is P2SH
+ + BASE58_CHARS
+ + `{33}`, // All min-max lengths are 34
+ bech32: `(?:`
+ + `(?:` // bech32 liquid starts with ex or lq
+ + `ex`
+ + `|`
+ + `lq`
+ + `)`
+ + BECH32_CHARS_LW // blech32 and bech32 are the same alphabet and protocol, different checksums.
+ + `{6,100}`
+ + `|`
+ + `(?:` // Same as above but all upper case
+ + `EX`
+ + `|`
+ + `LQ`
+ + `)`
+ + BECH32_CHARS_UP
+ + `{6,100}`
+ + `)`,
+ },
+ liquidtestnet: {
+ base58: `[89]` // ???(TODO: find version) is P2PKH, 8|9 is P2SH
+ + BASE58_CHARS
+ + `{33}`, // P2PKH is ???(TODO: find size), P2SH is 34
+ bech32: `(?:`
+ + `(?:` // bech32 liquid testnet starts with tex or tlq
+ + `tex` // TODO: Why does mempool use this and not ert|el like in the elements source?
+ + `|`
+ + `tlq` // TODO: does this exist?
+ + `)`
+ + BECH32_CHARS_LW // blech32 and bech32 are the same alphabet and protocol, different checksums.
+ + `{6,100}`
+ + `|`
+ + `(?:` // Same as above but all upper case
+ + `TEX`
+ + `|`
+ + `TLQ`
+ + `)`
+ + BECH32_CHARS_UP
+ + `{6,100}`
+ + `)`,
+ },
+ bisq: {
+ base58: `B1` // bisq base58 addrs start with B1
+ + BASE58_CHARS
+ + `{33}`, // always length 35
+ bech32: `(?:`
+ + `bbc1` // Starts with bbc1
+ + BECH32_CHARS_LW
+ + `{6,100}`
+ + `|`
+ + `BBC1` // All upper case version
+ + BECH32_CHARS_UP
+ + `{6,100}`
+ + `)`,
+ },
+}
+type RegexTypeNoAddr = `blockhash` | `transaction` | `blockheight`;
+export type RegexType = `address` | RegexTypeNoAddr;
+
+export const NETWORKS = [`testnet`, `signet`, `liquid`, `liquidtestnet`, `bisq`, `mainnet`] as const;
+export type Network = typeof NETWORKS[number]; // Turn const array into union type
+
+export const ADDRESS_REGEXES: [RegExp, Network][] = NETWORKS
+ .map(network => [getRegex('address', network), network])
+
+export function findOtherNetworks(address: string, skipNetwork: Network): Network[] {
+ return ADDRESS_REGEXES.filter(([regex, network]) =>
+ network !== skipNetwork &&
+ regex.test(address)
+ ).map(([, network]) => network);
+}
+
+export function getRegex(type: RegexTypeNoAddr): RegExp;
+export function getRegex(type: 'address', network: Network): RegExp;
+export function getRegex(type: RegexType, network?: Network): RegExp {
+ let regex = `^`; // ^ = Start of string
+ switch (type) {
+ // Match a block height number
+ // [Testing Order]: any order is fine
+ case `blockheight`:
+ regex += ZERO_INDEX_NUMBER_CHARS; // block height is a 0 indexed number
+ break;
+ // Match a 32 byte block hash in hex. Assumes at least 32 bits of difficulty.
+ // [Testing Order]: Must always be tested before `transaction`
+ case `blockhash`:
+ regex += `0{8}`; // Starts with exactly 8 zeroes in a row
+ regex += `${HEX_CHARS}{56}`; // Continues with exactly 56 hex letters/numbers
+ break;
+ // Match a 32 byte tx hash in hex. Contains optional output index specifier.
+ // [Testing Order]: Must always be tested after `blockhash`
+ case `transaction`:
+ regex += `${HEX_CHARS}{64}`; // Exactly 64 hex letters/numbers
+ regex += `(?:`; // Start a non-capturing group
+ regex += `:`; // 1 instances of the symbol ":"
+ regex += ZERO_INDEX_NUMBER_CHARS; // A zero indexed number
+ regex += `)?`; // End the non-capturing group. This group appears 0 or 1 times
+ break;
+ // Match any one of the many address types
+ // [Testing Order]: While possible that a bech32 address happens to be 64 hex
+ // characters in the future (current lengths are not 64), it is highly unlikely
+ // Order therefore, does not matter.
+ case `address`:
+ if (!network) {
+ throw new Error(`Must pass network when type is address`);
+ }
+ regex += `(?:`; // Start a non-capturing group (each network has multiple options)
+ switch (network) {
+ case `mainnet`:
+ regex += ADDRESS_CHARS.mainnet.base58;
+ regex += `|`; // OR
+ regex += ADDRESS_CHARS.mainnet.bech32;
+ break;
+ case `testnet`:
+ regex += ADDRESS_CHARS.testnet.base58;
+ regex += `|`; // OR
+ regex += ADDRESS_CHARS.testnet.bech32;
+ break;
+ case `signet`:
+ regex += ADDRESS_CHARS.signet.base58;
+ regex += `|`; // OR
+ regex += ADDRESS_CHARS.signet.bech32;
+ break;
+ case `liquid`:
+ regex += ADDRESS_CHARS.liquid.base58;
+ regex += `|`; // OR
+ regex += ADDRESS_CHARS.liquid.bech32;
+ break;
+ case `liquidtestnet`:
+ regex += ADDRESS_CHARS.liquidtestnet.base58;
+ regex += `|`; // OR
+ regex += ADDRESS_CHARS.liquidtestnet.bech32;
+ break;
+ case `bisq`:
+ regex += ADDRESS_CHARS.bisq.base58;
+ regex += `|`; // OR
+ regex += ADDRESS_CHARS.bisq.bech32;
+ break;
+ default:
+ throw new Error(`Invalid Network ${network} (Unreachable error in TypeScript)`);
+ }
+ regex += `)`; // End the non-capturing group
+ break;
+ default:
+ throw new Error(`Invalid RegexType ${type} (Unreachable error in TypeScript)`);
+ }
+ regex += `$`; // $ = End of string
+ return new RegExp(regex);
+}
From 2c59992d3fbc28f64312802c622564b97a946cd6 Mon Sep 17 00:00:00 2001
From: junderw
Date: Sun, 4 Sep 2022 21:53:52 +0900
Subject: [PATCH 011/639] Fix E2E error
---
.../search-form/search-form.component.ts | 2 +-
frontend/src/app/shared/common.utils.ts | 218 ------------------
2 files changed, 1 insertion(+), 219 deletions(-)
diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts
index a9e31221a..18b4048ef 100644
--- a/frontend/src/app/components/search-form/search-form.component.ts
+++ b/frontend/src/app/components/search-form/search-form.component.ts
@@ -65,7 +65,7 @@ export class SearchFormComponent implements OnInit {
this.stateService.networkChanged$.subscribe((network) => {
this.network = network;
// TODO: Eventually change network type here from string to enum of consts
- this.regexAddress = getRegex('address', network as any);
+ this.regexAddress = getRegex('address', network as any || 'mainnet');
});
this.searchForm = this.formBuilder.group({
diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts
index e50ba13b7..87c952c31 100644
--- a/frontend/src/app/shared/common.utils.ts
+++ b/frontend/src/app/shared/common.utils.ts
@@ -136,221 +136,3 @@ export function haversineDistance(lat1: number, lon1: number, lat2: number, lon2
export function kmToMiles(km: number): number {
return km * 0.62137119;
}
-
-// all base58 characters
-const BASE58_CHARS = `[a-km-zA-HJ-NP-Z1-9]`;
-
-// all bech32 characters (after the separator)
-const BECH32_CHARS_LW = `[ac-hj-np-z02-9]`;
-const BECH32_CHARS_UP = `[AC-HJ-NP-Z02-9]`;
-
-// Hex characters
-const HEX_CHARS = `[a-fA-F0-9]`;
-
-// A regex to say "A single 0 OR any number with no leading zeroes"
-// Capped at 13 digits so as to not be confused with lightning channel IDs (which are around 17 digits)
-// (?: // Start a non-capturing group
-// 0 // A single 0
-// | // OR
-// [1-9][0-9]{0,12} // Any succession of numbers up to 13 digits starting with 1-9
-// ) // End the non-capturing group.
-const ZERO_INDEX_NUMBER_CHARS = `(?:0|[1-9][0-9]{0,12})`;
-
-// Formatting of the address regex is for readability,
-// We should ignore formatting it with automated formatting tools like prettier.
-//
-// prettier-ignore
-const ADDRESS_CHARS: {
- [k in Network]: {
- base58: string;
- bech32: string;
- };
-} = {
- mainnet: {
- base58: `[13]` // Starts with a single 1 or 3
- + BASE58_CHARS
- + `{26,33}`, // Repeat the previous char 26-33 times.
- // Version byte 0x00 (P2PKH) can be as short as 27 characters, up to 34 length
- // P2SH must be 34 length
- bech32: `(?:`
- + `bc1` // Starts with bc1
- + BECH32_CHARS_LW
- + `{6,100}` // As per bech32, 6 char checksum is minimum
- + `|`
- + `BC1` // All upper case version
- + BECH32_CHARS_UP
- + `{6,100}`
- + `)`,
- },
- testnet: {
- base58: `[mn2]` // Starts with a single m, n, or 2 (P2PKH is m or n, 2 is P2SH)
- + BASE58_CHARS
- + `{33,34}`, // m|n is 34 length, 2 is 35 length (We match the first letter separately)
- bech32: `(?:`
- + `tb1` // Starts with bc1
- + BECH32_CHARS_LW
- + `{6,100}` // As per bech32, 6 char checksum is minimum
- + `|`
- + `TB1` // All upper case version
- + BECH32_CHARS_UP
- + `{6,100}`
- + `)`,
- },
- signet: {
- base58: `[mn2]`
- + BASE58_CHARS
- + `{33,34}`,
- bech32: `(?:`
- + `tb1` // Starts with tb1
- + BECH32_CHARS_LW
- + `{6,100}`
- + `|`
- + `TB1` // All upper case version
- + BECH32_CHARS_UP
- + `{6,100}`
- + `)`,
- },
- liquid: {
- base58: `[GHPQ]` // G|H is P2PKH, P|Q is P2SH
- + BASE58_CHARS
- + `{33}`, // All min-max lengths are 34
- bech32: `(?:`
- + `(?:` // bech32 liquid starts with ex or lq
- + `ex`
- + `|`
- + `lq`
- + `)`
- + BECH32_CHARS_LW // blech32 and bech32 are the same alphabet and protocol, different checksums.
- + `{6,100}`
- + `|`
- + `(?:` // Same as above but all upper case
- + `EX`
- + `|`
- + `LQ`
- + `)`
- + BECH32_CHARS_UP
- + `{6,100}`
- + `)`,
- },
- liquidtestnet: {
- base58: `[89]` // ???(TODO: find version) is P2PKH, 8|9 is P2SH
- + BASE58_CHARS
- + `{33}`, // P2PKH is ???(TODO: find size), P2SH is 34
- bech32: `(?:`
- + `(?:` // bech32 liquid testnet starts with tex or tlq
- + `tex` // TODO: Why does mempool use this and not ert|el like in the elements source?
- + `|`
- + `tlq` // TODO: does this exist?
- + `)`
- + BECH32_CHARS_LW // blech32 and bech32 are the same alphabet and protocol, different checksums.
- + `{6,100}`
- + `|`
- + `(?:` // Same as above but all upper case
- + `TEX`
- + `|`
- + `TLQ`
- + `)`
- + BECH32_CHARS_UP
- + `{6,100}`
- + `)`,
- },
- bisq: {
- base58: `B1` // bisq base58 addrs start with B1
- + BASE58_CHARS
- + `{33}`, // always length 35
- bech32: `(?:`
- + `bbc1` // Starts with bbc1
- + BECH32_CHARS_LW
- + `{6,100}`
- + `|`
- + `BBC1` // All upper case version
- + BECH32_CHARS_UP
- + `{6,100}`
- + `)`,
- },
-}
-type RegexTypeNoAddr = `blockhash` | `transaction` | `blockheight`;
-export type RegexType = `address` | RegexTypeNoAddr;
-
-export const NETWORKS = [`testnet`, `signet`, `liquid`, `liquidtestnet`, `bisq`, `mainnet`] as const;
-export type Network = typeof NETWORKS[number]; // Turn const array into union type
-
-export const ADDRESS_REGEXES: [RegExp, Network][] = NETWORKS
- .map(network => [getRegex('address', network), network])
-
-export function getRegex(type: RegexTypeNoAddr): RegExp;
-export function getRegex(type: 'address', network: Network): RegExp;
-export function getRegex(type: RegexType, network?: Network): RegExp {
- let regex = `^`; // ^ = Start of string
- switch (type) {
- // Match a block height number
- // [Testing Order]: any order is fine
- case `blockheight`:
- regex += ZERO_INDEX_NUMBER_CHARS; // block height is a 0 indexed number
- break;
- // Match a 32 byte block hash in hex. Assumes at least 32 bits of difficulty.
- // [Testing Order]: Must always be tested before `transaction`
- case `blockhash`:
- regex += `0{8}`; // Starts with exactly 8 zeroes in a row
- regex += `${HEX_CHARS}{56}`; // Continues with exactly 56 hex letters/numbers
- break;
- // Match a 32 byte tx hash in hex. Contains optional output index specifier.
- // [Testing Order]: Must always be tested after `blockhash`
- case `transaction`:
- regex += `${HEX_CHARS}{64}`; // Exactly 64 hex letters/numbers
- regex += `(?:`; // Start a non-capturing group
- regex += `:`; // 1 instances of the symbol ":"
- regex += ZERO_INDEX_NUMBER_CHARS; // A zero indexed number
- regex += `)?`; // End the non-capturing group. This group appears 0 or 1 times
- break;
- // Match any one of the many address types
- // [Testing Order]: While possible that a bech32 address happens to be 64 hex
- // characters in the future (current lengths are not 64), it is highly unlikely
- // Order therefore, does not matter.
- case `address`:
- if (!network) {
- throw new Error(`Must pass network when type is address`);
- }
- regex += `(?:`; // Start a non-capturing group (each network has multiple options)
- switch (network) {
- case `mainnet`:
- regex += ADDRESS_CHARS.mainnet.base58;
- regex += `|`; // OR
- regex += ADDRESS_CHARS.mainnet.bech32;
- break;
- case `testnet`:
- regex += ADDRESS_CHARS.testnet.base58;
- regex += `|`; // OR
- regex += ADDRESS_CHARS.testnet.bech32;
- break;
- case `signet`:
- regex += ADDRESS_CHARS.signet.base58;
- regex += `|`; // OR
- regex += ADDRESS_CHARS.signet.bech32;
- break;
- case `liquid`:
- regex += ADDRESS_CHARS.liquid.base58;
- regex += `|`; // OR
- regex += ADDRESS_CHARS.liquid.bech32;
- break;
- case `liquidtestnet`:
- regex += ADDRESS_CHARS.liquidtestnet.base58;
- regex += `|`; // OR
- regex += ADDRESS_CHARS.liquidtestnet.bech32;
- break;
- case `bisq`:
- regex += ADDRESS_CHARS.bisq.base58;
- regex += `|`; // OR
- regex += ADDRESS_CHARS.bisq.bech32;
- break;
- default:
- throw new Error(`Invalid Network ${network} (Unreachable error in TypeScript)`);
- }
- regex += `)`; // End the non-capturing group
- break;
- default:
- throw new Error(`Invalid RegexType ${type} (Unreachable error in TypeScript)`);
- }
- regex += `$`; // $ = End of string
- return new RegExp(regex);
-}
From 213800f563a2731a991cfbe2ce76b3ec419a92ad Mon Sep 17 00:00:00 2001
From: softsimon
Date: Wed, 19 Jul 2023 16:46:02 +0900
Subject: [PATCH 012/639] Merge error fix
---
.../app/components/search-form/search-form.component.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts
index 18b4048ef..eb8adbd83 100644
--- a/frontend/src/app/components/search-form/search-form.component.ts
+++ b/frontend/src/app/components/search-form/search-form.component.ts
@@ -35,10 +35,10 @@ export class SearchFormComponent implements OnInit {
}
}
- regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59})$/;
- regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
- regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
- regexBlockheight = /^[0-9]{1,9}$/;
+ regexAddress = getRegex('address', 'mainnet'); // Default to mainnet
+ regexBlockhash = getRegex('blockhash');
+ regexTransaction = getRegex('transaction');
+ regexBlockheight = getRegex('blockheight');
focus$ = new Subject();
click$ = new Subject();
From d7ac326f920ae40fe7e93c140ad3760ac5ab4d98 Mon Sep 17 00:00:00 2001
From: nymkappa <1612910616@pm.me>
Date: Sun, 23 Jul 2023 12:02:04 +0900
Subject: [PATCH 013/639] [block list] improve block list when db = false
---
.../blocks-list/blocks-list.component.html | 66 ++++++++++---------
.../blocks-list/blocks-list.component.ts | 17 +++--
2 files changed, 46 insertions(+), 37 deletions(-)
diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.html b/frontend/src/app/components/blocks-list/blocks-list.component.html
index 39fbb95e0..6248e6868 100644
--- a/frontend/src/app/components/blocks-list/blocks-list.component.html
+++ b/frontend/src/app/components/blocks-list/blocks-list.component.html
@@ -1,6 +1,6 @@
-
+
Blocks
@@ -9,28 +9,28 @@
- Height
- Height
+ Pool
- Timestamp
- Timestamp
+ Health
- Reward
- Fees
-
- Fees
+
+ TXs
- Transactions
- Size
+ Transactions
+ Size
{{ block.height }}
-
-
+
+
+
-
+
{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
-
+
Unknown
-
+
-
+
-
+
= 0" [class.negative]="block.extras.feeDelta < 0">
{{ block.extras.feeDelta > 0 ? '+' : '' }}{{ (block.extras.feeDelta * 100) | amountShortener: 2 }}%
-
+
{{ block.tx_count | number }}
-
+
@@ -82,34 +88,34 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.ts b/frontend/src/app/components/blocks-list/blocks-list.component.ts
index 1af6572fc..b1c40ec6f 100644
--- a/frontend/src/app/components/blocks-list/blocks-list.component.ts
+++ b/frontend/src/app/components/blocks-list/blocks-list.component.ts
@@ -17,6 +17,7 @@ export class BlocksList implements OnInit {
blocks$: Observable = undefined;
+ isMempoolModule = false;
indexingAvailable = false;
auditAvailable = false;
isLoading = true;
@@ -36,6 +37,7 @@ export class BlocksList implements OnInit {
public stateService: StateService,
private cd: ChangeDetectorRef,
) {
+ this.isMempoolModule = this.stateService.env.BASE_MODULE === 'mempool';
}
ngOnInit(): void {
@@ -64,11 +66,10 @@ export class BlocksList implements OnInit {
this.lastBlockHeight = Math.max(...blocks.map(o => o.height));
}),
map(blocks => {
- if (this.indexingAvailable) {
+ if (this.stateService.env.BASE_MODULE === 'mempool') {
for (const block of blocks) {
// @ts-ignore: Need to add an extra field for the template
- block.extras.pool.logo = `/resources/mining-pools/` +
- block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
+ block.extras.pool.logo = `/resources/mining-pools/` + block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
}
}
if (this.widget) {
@@ -99,7 +100,7 @@ export class BlocksList implements OnInit {
}
if (blocks[1]) {
this.blocksCount = Math.max(this.blocksCount, blocks[1][0].height) + 1;
- if (this.stateService.env.MINING_DASHBOARD) {
+ if (this.isMempoolModule) {
// @ts-ignore: Need to add an extra field for the template
blocks[1][0].extras.pool.logo = `/resources/mining-pools/` +
blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
@@ -110,9 +111,11 @@ export class BlocksList implements OnInit {
return acc;
}, []),
switchMap((blocks) => {
- blocks.forEach(block => {
- block.extras.feeDelta = block.extras.expectedFees ? (block.extras.totalFees - block.extras.expectedFees) / block.extras.expectedFees : 0;
- });
+ if (this.isMempoolModule && this.auditAvailable) {
+ blocks.forEach(block => {
+ block.extras.feeDelta = block.extras.expectedFees ? (block.extras.totalFees - block.extras.expectedFees) / block.extras.expectedFees : 0;
+ });
+ }
return of(blocks);
})
);
From 597073c9b6ae5eae166690aa7fcf1e3c5745a292 Mon Sep 17 00:00:00 2001
From: nymkappa <1612910616@pm.me>
Date: Sun, 23 Jul 2023 12:18:21 +0900
Subject: [PATCH 014/639] [block list] re-enable block fee if !auditAvailable
in widget
---
.../src/app/components/blocks-list/blocks-list.component.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.html b/frontend/src/app/components/blocks-list/blocks-list.component.html
index 6248e6868..85e2ea17f 100644
--- a/frontend/src/app/components/blocks-list/blocks-list.component.html
+++ b/frontend/src/app/components/blocks-list/blocks-list.component.html
@@ -17,7 +17,7 @@
i18n-ngbTooltip="latest-blocks.health" ngbTooltip="Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Health
Reward
- Fees
+ Fees
TXs
@@ -65,7 +65,7 @@
-
+
From f7ec5ca82ee95d0f9e5559a9f578ec463f77d083 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?dni=20=E2=9A=A1?=
Date: Thu, 3 Aug 2023 17:47:30 +0200
Subject: [PATCH 015/639] Update liquid-master-page.component.html
ngIf for BISQ_ENABLED was missing
---
.../liquid-master-page/liquid-master-page.component.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html
index 476c78622..50296b895 100644
--- a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html
+++ b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html
@@ -53,7 +53,7 @@
Signet
Testnet
- Bisq
+ Bisq
Liquid
Liquid Testnet
@@ -98,4 +98,4 @@
-
\ No newline at end of file
+
From 223f6df3713729eb02dbedd4eefdf16a43c53dc9 Mon Sep 17 00:00:00 2001
From: nymkappa <1612910616@pm.me>
Date: Sat, 5 Aug 2023 10:52:11 +0900
Subject: [PATCH 016/639] fix 'large' class trigger
---
.../transactions-list/transactions-list.component.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html
index ef34bf822..24d34d6ec 100644
--- a/frontend/src/app/components/transactions-list/transactions-list.component.html
+++ b/frontend/src/app/components/transactions-list/transactions-list.component.html
@@ -81,7 +81,7 @@
- 1000000}">
+ 1000000000}">
@@ -222,7 +222,7 @@
- 1000000}">
+ 1000000000}">
From fc73dcc3444b73b31110c45734a0155524302685 Mon Sep 17 00:00:00 2001
From: Stephan Oeste
Date: Mon, 7 Aug 2023 20:08:30 +0200
Subject: [PATCH 017/639] Fixing proxy_buffer_size error nginx.conf
---
production/nginx/http-proxy-cache.conf | 1 +
1 file changed, 1 insertion(+)
diff --git a/production/nginx/http-proxy-cache.conf b/production/nginx/http-proxy-cache.conf
index f870939b3..60c6d4f82 100644
--- a/production/nginx/http-proxy-cache.conf
+++ b/production/nginx/http-proxy-cache.conf
@@ -3,3 +3,4 @@ proxy_cache_path /var/cache/nginx/api keys_zone=api:20m levels=1:2 inactive=600s
proxy_cache_path /var/cache/nginx/services keys_zone=services:20m levels=1:2 inactive=600s max_size=100m;
proxy_cache_path /var/cache/nginx/markets keys_zone=markets:20m levels=1:2 inactive=600s max_size=100m;
types_hash_max_size 2048;
+proxy_buffer_size 8k;
From 89be841e6429d3900ace94a7b1cd9cbc44dbfee8 Mon Sep 17 00:00:00 2001
From: nymkappa <1612910616@pm.me>
Date: Tue, 8 Aug 2023 17:10:59 +0900
Subject: [PATCH 018/639] [accelerator] hide accelerate button if already
accelerating
---
.../app/components/transaction/transaction.component.html | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html
index 9584cecfd..0d8e4d4c3 100644
--- a/frontend/src/app/components/transaction/transaction.component.html
+++ b/frontend/src/app/components/transaction/transaction.component.html
@@ -101,7 +101,8 @@
= 7" [ngIfElse]="belowBlockLimit">
In several hours (or more)
- Accelerate
+
+ Accelerate
@@ -111,7 +112,8 @@
- Accelerate
+
+ Accelerate
From b988a4c526c4e76ff8f9ba493e689de2b8d05f73 Mon Sep 17 00:00:00 2001
From: nymkappa <1612910616@pm.me>
Date: Thu, 10 Aug 2023 18:01:05 +0900
Subject: [PATCH 019/639] use new services api to fetch chad/whale profile
image
---
frontend/src/app/components/about/about.component.html | 2 +-
frontend/src/resources/profile/unknown.svg | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html
index e9d5ec3b2..3b7647891 100644
--- a/frontend/src/app/components/about/about.component.html
+++ b/frontend/src/app/components/about/about.component.html
@@ -207,7 +207,7 @@
-
+
diff --git a/frontend/src/resources/profile/unknown.svg b/frontend/src/resources/profile/unknown.svg
index 50a548c3c..236f054c4 100644
--- a/frontend/src/resources/profile/unknown.svg
+++ b/frontend/src/resources/profile/unknown.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
From 6199216c5407364354e32ee5776c12ddbdfced1b Mon Sep 17 00:00:00 2001
From: nymkappa <1612910616@pm.me>
Date: Thu, 10 Aug 2023 18:08:30 +0900
Subject: [PATCH 020/639] use new services api to fetch chads profile image as
well
---
frontend/src/app/components/about/about.component.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html
index 3b7647891..e9e87d1c1 100644
--- a/frontend/src/app/components/about/about.component.html
+++ b/frontend/src/app/components/about/about.component.html
@@ -219,7 +219,7 @@
From b3f90e298127ad0199ca3c0312fb3022ecfd4a28 Mon Sep 17 00:00:00 2001
From: hunicus <93150691+hunicus@users.noreply.github.com>
Date: Sun, 13 Aug 2023 08:23:57 +0900
Subject: [PATCH 021/639] Add mempool enterprise note to top-level readme
---
README.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index dd2e62478..9bc988970 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,9 @@ It is an open-source project developed and operated for the benefit of the Bitco
Mempool can be self-hosted on a wide variety of your own hardware, ranging from a simple one-click installation on a Raspberry Pi full-node distro all the way to a robust production instance on a powerful FreeBSD server.
-**Most people should use a one-click install method.** Other install methods are meant for developers and others with experience managing servers.
+Most people should use a one-click install method .
+
+Other install methods are meant for developers and others with experience managing servers. If you want support for your own production instance of Mempool, or if you'd like to have your own instance of Mempool run by the mempool.space team on their own global ISP infrastructure—check out Mempool Enterprise® .
## One-Click Installation
From 102579baa972b80e08b637d034dfe54804cb7b9b Mon Sep 17 00:00:00 2001
From: hunicus <93150691+hunicus@users.noreply.github.com>
Date: Sun, 13 Aug 2023 13:00:47 +0900
Subject: [PATCH 022/639] Add enterprise note to production readme
---
production/README.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/production/README.md b/production/README.md
index 87b8bb0a1..8e325bb1b 100644
--- a/production/README.md
+++ b/production/README.md
@@ -2,7 +2,9 @@
These instructions are for setting up a serious production Mempool website for Bitcoin (mainnet, testnet, signet), Liquid (mainnet, testnet), and Bisq.
-Again, this setup is no joke—home users should use [one of the other installation methods](../#installation-methods). Support is only provided to [enterprise sponsors](https://mempool.space/enterprise).
+Again, this setup is no joke—home users should use [one of the other installation methods](../#installation-methods). Support is only provided to project sponsors through [Mempool Enterprise®](https://mempool.space/enterprise).
+
+You can also have the mempool.space team run a highly-performant and highly-available instance of Mempool for you on their own global ISP infrastructure. See Mempool Enterprise® for more details.
### Server Hardware
From 0a918b8fa84270754ee3afa47c5dd9acca4c54e7 Mon Sep 17 00:00:00 2001
From: hunicus <93150691+hunicus@users.noreply.github.com>
Date: Sun, 13 Aug 2023 13:04:58 +0900
Subject: [PATCH 023/639] Add enterprise note to backend readme
---
backend/README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/README.md b/backend/README.md
index 6a0cb821c..0582aca8c 100644
--- a/backend/README.md
+++ b/backend/README.md
@@ -2,7 +2,7 @@
These instructions are mostly intended for developers.
-If you choose to use these instructions for a production setup, be aware that you will still probably need to do additional configuration for your specific OS, environment, use-case, etc. We do our best here to provide a good starting point, but only proceed if you know what you're doing. Mempool only provides support for custom setups to [enterprise sponsors](https://mempool.space/enterprise).
+If you choose to use these instructions for a production setup, be aware that you will still probably need to do additional configuration for your specific OS, environment, use-case, etc. We do our best here to provide a good starting point, but only proceed if you know what you're doing. Mempool only provides support for custom setups to project sponsors through [Mempool Enterprise®](https://mempool.space/enterprise).
See other ways to set up Mempool on [the main README](/../../#installation-methods).
From 1dd66e669544a1e6f8d19fd135f96909979c3f78 Mon Sep 17 00:00:00 2001
From: hunicus <93150691+hunicus@users.noreply.github.com>
Date: Wed, 16 Aug 2023 12:43:36 +0900
Subject: [PATCH 024/639] Add bimi svg
---
frontend/src/resources/bimi.svg | 1 +
1 file changed, 1 insertion(+)
create mode 100644 frontend/src/resources/bimi.svg
diff --git a/frontend/src/resources/bimi.svg b/frontend/src/resources/bimi.svg
new file mode 100644
index 000000000..78a877552
--- /dev/null
+++ b/frontend/src/resources/bimi.svg
@@ -0,0 +1 @@
+Mempool Space K.K.
\ No newline at end of file
From 8ea7bb907c519002ae17ccfaae52075ed6a2525f Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Tue, 22 Aug 2023 02:40:00 +0900
Subject: [PATCH 025/639] Fix js error on mouseover on difficulty skeleton
---
.../src/app/components/difficulty/difficulty.component.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/frontend/src/app/components/difficulty/difficulty.component.ts b/frontend/src/app/components/difficulty/difficulty.component.ts
index 7f305416f..81084f524 100644
--- a/frontend/src/app/components/difficulty/difficulty.component.ts
+++ b/frontend/src/app/components/difficulty/difficulty.component.ts
@@ -194,7 +194,7 @@ export class DifficultyComponent implements OnInit {
@HostListener('pointerdown', ['$event'])
onPointerDown(event): void {
- if (this.epochSvgElement.nativeElement?.contains(event.target)) {
+ if (this.epochSvgElement?.nativeElement?.contains(event.target)) {
this.onPointerMove(event);
event.preventDefault();
}
@@ -202,7 +202,7 @@ export class DifficultyComponent implements OnInit {
@HostListener('pointermove', ['$event'])
onPointerMove(event): void {
- if (this.epochSvgElement.nativeElement?.contains(event.target)) {
+ if (this.epochSvgElement?.nativeElement?.contains(event.target)) {
this.tooltipPosition = { x: event.clientX, y: event.clientY };
this.cd.markForCheck();
}
From b7474b29e4cbe588e61f4f3e8f85568e3df40145 Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Wed, 23 Aug 2023 00:33:43 +0900
Subject: [PATCH 026/639] Use symlink to avoid duplicate fallback images
---
unfurler/src/index.ts | 2 +-
unfurler/src/resources | 1 +
unfurler/src/resources/img/bisq.png | Bin 96581 -> 0 bytes
unfurler/src/resources/img/dashboard.png | Bin 295855 -> 0 bytes
unfurler/src/resources/img/lightning.png | Bin 1853805 -> 0 bytes
unfurler/src/resources/img/liquid.png | Bin 98482 -> 0 bytes
unfurler/src/resources/img/mempool.png | Bin 295855 -> 0 bytes
unfurler/src/resources/img/mining.png | Bin 621216 -> 0 bytes
unfurler/src/routes.ts | 9 ---------
9 files changed, 2 insertions(+), 10 deletions(-)
create mode 120000 unfurler/src/resources
delete mode 100644 unfurler/src/resources/img/bisq.png
delete mode 100644 unfurler/src/resources/img/dashboard.png
delete mode 100644 unfurler/src/resources/img/lightning.png
delete mode 100644 unfurler/src/resources/img/liquid.png
delete mode 100644 unfurler/src/resources/img/mempool.png
delete mode 100644 unfurler/src/resources/img/mining.png
diff --git a/unfurler/src/index.ts b/unfurler/src/index.ts
index 3c7d3cc92..e3e8e07eb 100644
--- a/unfurler/src/index.ts
+++ b/unfurler/src/index.ts
@@ -251,7 +251,7 @@ class Server {
if (!img) {
// send local fallback image file
- res.sendFile(nodejsPath.join(__dirname, matchedRoute.fallbackFile));
+ res.sendFile(nodejsPath.join(__dirname, matchedRoute.fallbackImg));
} else {
res.contentType('image/png');
res.send(img);
diff --git a/unfurler/src/resources b/unfurler/src/resources
new file mode 120000
index 000000000..aadffa9d9
--- /dev/null
+++ b/unfurler/src/resources
@@ -0,0 +1 @@
+../../frontend/src/resources
\ No newline at end of file
diff --git a/unfurler/src/resources/img/bisq.png b/unfurler/src/resources/img/bisq.png
deleted file mode 100644
index 2b5e1250b9d5a69cd25c9de9efdeb53a1f125f46..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 96581
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