Merge branch 'master' into bugfix/fix-duplicate-indexing

This commit is contained in:
wiz 2022-02-07 14:55:38 +00:00 committed by GitHub
commit 4b871468bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 1229 additions and 368 deletions

View file

@ -26,7 +26,7 @@ jobs:
with: with:
working-directory: frontend working-directory: frontend
build: npm run config:defaults:mempool build: npm run config:defaults:mempool
start: npm run start:local-prod start: npm run start:local-staging
wait-on: 'http://localhost:4200' wait-on: 'http://localhost:4200'
wait-on-timeout: 120 wait-on-timeout: 120
record: true record: true
@ -49,7 +49,7 @@ jobs:
with: with:
working-directory: frontend working-directory: frontend
build: npm run config:defaults:liquid build: npm run config:defaults:liquid
start: npm run start:local-prod start: npm run start:local-staging
wait-on: 'http://localhost:4200' wait-on: 'http://localhost:4200'
wait-on-timeout: 120 wait-on-timeout: 120
record: true record: true
@ -71,7 +71,7 @@ jobs:
with: with:
working-directory: frontend working-directory: frontend
build: npm run config:defaults:bisq build: npm run config:defaults:bisq
start: npm run start:local-prod start: npm run start:local-staging
wait-on: 'http://localhost:4200' wait-on: 'http://localhost:4200'
wait-on-timeout: 120 wait-on-timeout: 120
record: true record: true

View file

@ -88,9 +88,10 @@ JSON:
"BLOCK_WEIGHT_UNITS": 4000000, "BLOCK_WEIGHT_UNITS": 4000000,
"INITIAL_BLOCKS_AMOUNT": 8, "INITIAL_BLOCKS_AMOUNT": 8,
"MEMPOOL_BLOCKS_AMOUNT": 8, "MEMPOOL_BLOCKS_AMOUNT": 8,
"PRICE_FEED_UPDATE_INTERVAL": 3600, "PRICE_FEED_UPDATE_INTERVAL": 600,
"USE_SECOND_NODE_FOR_MINFEE": false, "USE_SECOND_NODE_FOR_MINFEE": false,
"EXTERNAL_ASSETS": [] "EXTERNAL_ASSETS": ["https://mempool.space/resources/pools.json"],
"STDOUT_LOG_MIN_PRIORITY": "debug"
}, },
``` ```
@ -111,6 +112,7 @@ docker-compose overrides::
MEMPOOL_PRICE_FEED_UPDATE_INTERVAL: "" MEMPOOL_PRICE_FEED_UPDATE_INTERVAL: ""
MEMPOOL_USE_SECOND_NODE_FOR_MINFEE: "" MEMPOOL_USE_SECOND_NODE_FOR_MINFEE: ""
MEMPOOL_EXTERNAL_ASSETS: "" MEMPOOL_EXTERNAL_ASSETS: ""
MEMPOOL_STDOUT_LOG_MIN_PRIORITY: ""
``` ```
JSON: JSON:
@ -245,6 +247,39 @@ docker-compose overrides:
BISQ_DATA_PATH: "" BISQ_DATA_PATH: ""
``` ```
JSON:
```
"SOCKS5PROXY": {
"ENABLED": false,
"HOST": "127.0.0.1",
"PORT": "9050",
"USERNAME": "",
"PASSWORD": ""
}
```
docker-compose overrides:
```
SOCKS5PROXY_ENABLED: ""
SOCKS5PROXY_HOST: ""
SOCKS5PROXY_PORT: ""
SOCKS5PROXY_USERNAME: ""
SOCKS5PROXY_PASSWORD: ""
```
JSON:
```
"PRICE_DATA_SERVER": {
"TOR_URL": "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices",
"CLEARNET_URL": "https://price.bisq.wiz.biz/getAllMarketPrices"
}
```
docker-compose overrides:
```
PRICE_DATA_SERVER_TOR_URL: ""
PRICE_DATA_SERVER_CLEARNET_URL: ""
```
# Manual Installation # Manual Installation

View file

@ -13,11 +13,12 @@
"INITIAL_BLOCKS_AMOUNT": 8, "INITIAL_BLOCKS_AMOUNT": 8,
"MEMPOOL_BLOCKS_AMOUNT": 8, "MEMPOOL_BLOCKS_AMOUNT": 8,
"INDEXING_BLOCKS_AMOUNT": 1100, "INDEXING_BLOCKS_AMOUNT": 1100,
"PRICE_FEED_UPDATE_INTERVAL": 3600, "PRICE_FEED_UPDATE_INTERVAL": 600,
"USE_SECOND_NODE_FOR_MINFEE": false, "USE_SECOND_NODE_FOR_MINFEE": false,
"EXTERNAL_ASSETS": [ "EXTERNAL_ASSETS": [
"https://mempool.space/resources/pools.json" "https://mempool.space/resources/pools.json"
] ],
"STDOUT_LOG_MIN_PRIORITY": "debug"
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
@ -61,5 +62,16 @@
"BISQ": { "BISQ": {
"ENABLED": false, "ENABLED": false,
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db" "DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
},
"SOCKS5PROXY": {
"ENABLED": false,
"HOST": "127.0.0.1",
"PORT": 9050,
"USERNAME": "",
"PASSWORD": ""
},
"PRICE_DATA_SERVER": {
"TOR_URL": "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices",
"CLEARNET_URL": "https://price.bisq.wiz.biz/getAllMarketPrices"
} }
} }

View file

@ -19,6 +19,7 @@
"locutus": "^2.0.12", "locutus": "^2.0.12",
"mysql2": "2.3.3", "mysql2": "2.3.3",
"node-worker-threads-pool": "^1.4.3", "node-worker-threads-pool": "^1.4.3",
"socks-proxy-agent": "^6.1.1",
"typescript": "4.4.4", "typescript": "4.4.4",
"ws": "8.3.0" "ws": "8.3.0"
}, },
@ -181,6 +182,38 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/agent-base/node_modules/debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/agent-base/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/ansi-styles": { "node_modules/ansi-styles": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
@ -731,6 +764,11 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
}, },
"node_modules/ip": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
"integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo="
},
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -1189,6 +1227,62 @@
"sha.js": "bin.js" "sha.js": "bin.js"
} }
}, },
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.6.1.tgz",
"integrity": "sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA==",
"dependencies": {
"ip": "^1.1.5",
"smart-buffer": "^4.1.0"
},
"engines": {
"node": ">= 10.13.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks-proxy-agent": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz",
"integrity": "sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==",
"dependencies": {
"agent-base": "^6.0.2",
"debug": "^4.3.1",
"socks": "^2.6.1"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/socks-proxy-agent/node_modules/debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socks-proxy-agent/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/sprintf-js": { "node_modules/sprintf-js": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@ -1538,6 +1632,29 @@
"negotiator": "0.6.2" "negotiator": "0.6.2"
} }
}, },
"agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"requires": {
"debug": "4"
},
"dependencies": {
"debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"ansi-styles": { "ansi-styles": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
@ -1992,6 +2109,11 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
}, },
"ip": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
"integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo="
},
"ipaddr.js": { "ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -2352,6 +2474,45 @@
"safe-buffer": "^5.0.1" "safe-buffer": "^5.0.1"
} }
}, },
"smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="
},
"socks": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.6.1.tgz",
"integrity": "sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA==",
"requires": {
"ip": "^1.1.5",
"smart-buffer": "^4.1.0"
}
},
"socks-proxy-agent": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz",
"integrity": "sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==",
"requires": {
"agent-base": "^6.0.2",
"debug": "^4.3.1",
"socks": "^2.6.1"
},
"dependencies": {
"debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"sprintf-js": { "sprintf-js": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",

View file

@ -38,6 +38,7 @@
"locutus": "^2.0.12", "locutus": "^2.0.12",
"mysql2": "2.3.3", "mysql2": "2.3.3",
"node-worker-threads-pool": "^1.4.3", "node-worker-threads-pool": "^1.4.3",
"socks-proxy-agent": "^6.1.1",
"typescript": "4.4.4", "typescript": "4.4.4",
"ws": "8.3.0" "ws": "8.3.0"
}, },

View file

@ -1,7 +1,9 @@
import logger from '../logger'; import logger from '../logger';
import axios from 'axios'; import axios, { AxiosResponse } from 'axios';
import { IConversionRates } from '../mempool.interfaces'; import { IConversionRates } from '../mempool.interfaces';
import config from '../config'; import config from '../config';
import backendInfo from './backend-info';
import { SocksProxyAgent } from 'socks-proxy-agent';
class FiatConversion { class FiatConversion {
private conversionRates: IConversionRates = { private conversionRates: IConversionRates = {
@ -17,6 +19,11 @@ class FiatConversion {
public startService() { public startService() {
logger.info('Starting currency rates service'); logger.info('Starting currency rates service');
if (config.SOCKS5PROXY.ENABLED) {
logger.info(`Currency rates service will be queried over the Tor network using ${config.PRICE_DATA_SERVER.TOR_URL}`);
} else {
logger.info(`Currency rates service will be queried over clearnet using ${config.PRICE_DATA_SERVER.CLEARNET_URL}`);
}
setInterval(this.updateCurrency.bind(this), 1000 * config.MEMPOOL.PRICE_FEED_UPDATE_INTERVAL); setInterval(this.updateCurrency.bind(this), 1000 * config.MEMPOOL.PRICE_FEED_UPDATE_INTERVAL);
this.updateCurrency(); this.updateCurrency();
} }
@ -26,12 +33,43 @@ class FiatConversion {
} }
private async updateCurrency(): Promise<void> { private async updateCurrency(): Promise<void> {
const headers = { 'User-Agent': `mempool/v${backendInfo.getBackendInfo().version}` };
let fiatConversionUrl: string;
let response: AxiosResponse;
try { try {
const response = await axios.get('https://price.bisq.wiz.biz/getAllMarketPrices', { timeout: 10000 }); if (config.SOCKS5PROXY.ENABLED) {
let socksOptions: any = {
agentOptions: {
keepAlive: true,
},
host: config.SOCKS5PROXY.HOST,
port: config.SOCKS5PROXY.PORT
};
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
socksOptions.username = config.SOCKS5PROXY.USERNAME;
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
}
const agent = new SocksProxyAgent(socksOptions);
fiatConversionUrl = config.PRICE_DATA_SERVER.TOR_URL;
logger.debug('Querying currency rates service...');
response = await axios.get(fiatConversionUrl, { httpAgent: agent, headers: headers, timeout: 30000 });
} else {
fiatConversionUrl = config.PRICE_DATA_SERVER.CLEARNET_URL;
logger.debug('Querying currency rates service...');
response = await axios.get(fiatConversionUrl, { headers: headers, timeout: 10000 });
}
const usd = response.data.data.find((item: any) => item.currencyCode === 'USD'); const usd = response.data.data.find((item: any) => item.currencyCode === 'USD');
this.conversionRates = { this.conversionRates = {
'USD': usd.price, 'USD': usd.price,
}; };
logger.debug(`USD Conversion Rate: ${usd.price}`);
if (this.ratesChangedCallback) { if (this.ratesChangedCallback) {
this.ratesChangedCallback(this.conversionRates); this.ratesChangedCallback(this.conversionRates);
} }

View file

@ -18,6 +18,7 @@ interface IConfig {
PRICE_FEED_UPDATE_INTERVAL: number; PRICE_FEED_UPDATE_INTERVAL: number;
USE_SECOND_NODE_FOR_MINFEE: boolean; USE_SECOND_NODE_FOR_MINFEE: boolean;
EXTERNAL_ASSETS: string[]; EXTERNAL_ASSETS: string[];
STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
}; };
ESPLORA: { ESPLORA: {
REST_API_URL: string; REST_API_URL: string;
@ -51,7 +52,7 @@ interface IConfig {
ENABLED: boolean; ENABLED: boolean;
HOST: string; HOST: string;
PORT: number; PORT: number;
MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' |'warn' | 'notice' | 'info' | 'debug'; MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
FACILITY: string; FACILITY: string;
}; };
STATISTICS: { STATISTICS: {
@ -62,6 +63,17 @@ interface IConfig {
ENABLED: boolean; ENABLED: boolean;
DATA_PATH: string; DATA_PATH: string;
}; };
SOCKS5PROXY: {
ENABLED: boolean;
HOST: string;
PORT: number;
USERNAME: string;
PASSWORD: string;
};
PRICE_DATA_SERVER: {
TOR_URL: string;
CLEARNET_URL: string;
};
} }
const defaults: IConfig = { const defaults: IConfig = {
@ -79,11 +91,12 @@ const defaults: IConfig = {
'INITIAL_BLOCKS_AMOUNT': 8, 'INITIAL_BLOCKS_AMOUNT': 8,
'MEMPOOL_BLOCKS_AMOUNT': 8, 'MEMPOOL_BLOCKS_AMOUNT': 8,
'INDEXING_BLOCKS_AMOUNT': 1100, // 0 = disable indexing, -1 = index all blocks 'INDEXING_BLOCKS_AMOUNT': 1100, // 0 = disable indexing, -1 = index all blocks
'PRICE_FEED_UPDATE_INTERVAL': 3600, 'PRICE_FEED_UPDATE_INTERVAL': 600,
'USE_SECOND_NODE_FOR_MINFEE': false, 'USE_SECOND_NODE_FOR_MINFEE': false,
'EXTERNAL_ASSETS': [ 'EXTERNAL_ASSETS': [
'https://mempool.space/resources/pools.json' 'https://mempool.space/resources/pools.json'
] ],
'STDOUT_LOG_MIN_PRIORITY': 'debug',
}, },
'ESPLORA': { 'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000', 'REST_API_URL': 'http://127.0.0.1:3000',
@ -128,6 +141,17 @@ const defaults: IConfig = {
'ENABLED': false, 'ENABLED': false,
'DATA_PATH': '/bisq/statsnode-data/btc_mainnet/db' 'DATA_PATH': '/bisq/statsnode-data/btc_mainnet/db'
}, },
'SOCKS5PROXY': {
'ENABLED': false,
'HOST': '127.0.0.1',
'PORT': 9050,
'USERNAME': '',
'PASSWORD': ''
},
"PRICE_DATA_SERVER": {
'TOR_URL': 'http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices',
'CLEARNET_URL': 'https://price.bisq.wiz.biz/getAllMarketPrices'
}
}; };
class Config implements IConfig { class Config implements IConfig {
@ -140,6 +164,8 @@ class Config implements IConfig {
SYSLOG: IConfig['SYSLOG']; SYSLOG: IConfig['SYSLOG'];
STATISTICS: IConfig['STATISTICS']; STATISTICS: IConfig['STATISTICS'];
BISQ: IConfig['BISQ']; BISQ: IConfig['BISQ'];
SOCKS5PROXY: IConfig['SOCKS5PROXY'];
PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
constructor() { constructor() {
const configs = this.merge(configFile, defaults); const configs = this.merge(configFile, defaults);
@ -152,6 +178,8 @@ class Config implements IConfig {
this.SYSLOG = configs.SYSLOG; this.SYSLOG = configs.SYSLOG;
this.STATISTICS = configs.STATISTICS; this.STATISTICS = configs.STATISTICS;
this.BISQ = configs.BISQ; this.BISQ = configs.BISQ;
this.SOCKS5PROXY = configs.SOCKS5PROXY;
this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER;
} }
merge = (...objects: object[]): IConfig => { merge = (...objects: object[]): IConfig => {

View file

@ -319,7 +319,9 @@ class Server {
if (Common.isLiquid()) { if (Common.isLiquid()) {
this.app this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'assets/icons', routes.getAllLiquidIcon) .get(config.MEMPOOL.API_URL_PREFIX + 'assets/icons', routes.getAllLiquidIcon)
.get(config.MEMPOOL.API_URL_PREFIX + 'assets/featured', routes.$getAllFeaturedLiquidAssets)
.get(config.MEMPOOL.API_URL_PREFIX + 'asset/:assetId/icon', routes.getLiquidIcon) .get(config.MEMPOOL.API_URL_PREFIX + 'asset/:assetId/icon', routes.getLiquidIcon)
.get(config.MEMPOOL.API_URL_PREFIX + 'assets/group/:id', routes.$getAssetGroup)
; ;
} }

View file

@ -97,6 +97,9 @@ class Logger {
syslogmsg = `<${(Logger.facilities[config.SYSLOG.FACILITY] * 8 + prionum)}> ${this.name}[${process.pid}]: ${priority.toUpperCase()}${network} ${msg}`; syslogmsg = `<${(Logger.facilities[config.SYSLOG.FACILITY] * 8 + prionum)}> ${this.name}[${process.pid}]: ${priority.toUpperCase()}${network} ${msg}`;
this.syslog(syslogmsg); this.syslog(syslogmsg);
} }
if (Logger.priorities[priority] > Logger.priorities[config.MEMPOOL.STDOUT_LOG_MIN_PRIORITY]) {
return;
}
if (priority === 'warning') { if (priority === 'warning') {
priority = 'warn'; priority = 'warn';
} }

View file

@ -21,6 +21,7 @@ import bitcoinClient from './api/bitcoin/bitcoin-client';
import elementsParser from './api/liquid/elements-parser'; import elementsParser from './api/liquid/elements-parser';
import icons from './api/liquid/icons'; import icons from './api/liquid/icons';
import miningStats from './api/mining'; import miningStats from './api/mining';
import axios from 'axios';
class Routes { class Routes {
constructor() {} constructor() {}
@ -855,6 +856,25 @@ class Routes {
res.status(404).send('Asset icons not found'); res.status(404).send('Asset icons not found');
} }
} }
public async $getAllFeaturedLiquidAssets(req: Request, res: Response) {
try {
const response = await axios.get('https://liquid.network/api/v1/assets/featured', { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
}
public async $getAssetGroup(req: Request, res: Response) {
try {
const response = await axios.get('https://liquid.network/api/v1/assets/group/' + parseInt(req.params.id, 10),
{ responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
}
} }
export default new Routes(); export default new Routes();

View file

@ -10,7 +10,8 @@
"moduleResolution": "node", "moduleResolution": "node",
"typeRoots": [ "typeRoots": [
"node_modules/@types" "node_modules/@types"
] ],
"allowSyntheticDefaultImports": true
}, },
"include": [ "include": [
"src/**/*.ts" "src/**/*.ts"

View file

@ -14,7 +14,8 @@
"MEMPOOL_BLOCKS_AMOUNT": __MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__, "MEMPOOL_BLOCKS_AMOUNT": __MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__,
"PRICE_FEED_UPDATE_INTERVAL": __MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__, "PRICE_FEED_UPDATE_INTERVAL": __MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__,
"USE_SECOND_NODE_FOR_MINFEE": __MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__, "USE_SECOND_NODE_FOR_MINFEE": __MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__,
"EXTERNAL_ASSETS": __MEMPOOL_EXTERNAL_ASSETS__ "EXTERNAL_ASSETS": __MEMPOOL_EXTERNAL_ASSETS__,
"STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__"
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "__CORE_RPC_HOST__", "HOST": "__CORE_RPC_HOST__",
@ -58,5 +59,16 @@
"BISQ": { "BISQ": {
"ENABLED": __BISQ_ENABLED__, "ENABLED": __BISQ_ENABLED__,
"DATA_PATH": "__BISQ_DATA_PATH__" "DATA_PATH": "__BISQ_DATA_PATH__"
},
"SOCKS5PROXY": {
"ENABLED": __SOCKS5PROXY_ENABLED__,
"HOST": "__SOCKS5PROXY_HOST__",
"PORT": "__SOCKS5PROXY_PORT__",
"USERNAME": "__SOCKS5PROXY_USERNAME__",
"PASSWORD": "__SOCKS5PROXY_PASSWORD__"
},
"PRICE_DATA_SERVER": {
"TOR_URL": "__PRICE_DATA_SERVER_TOR_URL__",
"CLEARNET_URL": "__PRICE_DATA_SERVER_CLEARNET_URL__"
} }
} }

View file

@ -14,9 +14,10 @@ __MEMPOOL_BLOCK_WEIGHT_UNITS__=${MEMPOOL_BLOCK_WEIGHT_UNITS:=4000000}
__MEMPOOL_INITIAL_BLOCKS_AMOUNT__=${MEMPOOL_INITIAL_BLOCKS_AMOUNT:=8} __MEMPOOL_INITIAL_BLOCKS_AMOUNT__=${MEMPOOL_INITIAL_BLOCKS_AMOUNT:=8}
__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_MEMPOOL_BLOCKS_AMOUNT:=8} __MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_MEMPOOL_BLOCKS_AMOUNT:=8}
__MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=1100} __MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=1100}
__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__=${MEMPOOL_PRICE_FEED_UPDATE_INTERVAL:=3600} __MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__=${MEMPOOL_PRICE_FEED_UPDATE_INTERVAL:=600}
__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__=${MEMPOOL_USE_SECOND_NODE_FOR_MINFEE:=false} __MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__=${MEMPOOL_USE_SECOND_NODE_FOR_MINFEE:=false}
__MEMPOOL_EXTERNAL_ASSETS__=${MEMPOOL_EXTERNAL_ASSETS:=[]} __MEMPOOL_EXTERNAL_ASSETS__=${MEMPOOL_EXTERNAL_ASSETS:=[\"https://mempool.space/resources/pools.json\"]}
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=debug}
# CORE_RPC # CORE_RPC
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1} __CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
@ -61,6 +62,17 @@ __STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__=${STATISTICS_TX_PER_SECOND_SAMPLE_PER
__BISQ_ENABLED__=${BISQ_ENABLED:=false} __BISQ_ENABLED__=${BISQ_ENABLED:=false}
__BISQ_DATA_PATH__=${BISQ_DATA_PATH:=/bisq/statsnode-data/btc_mainnet/db} __BISQ_DATA_PATH__=${BISQ_DATA_PATH:=/bisq/statsnode-data/btc_mainnet/db}
# SOCKS5PROXY
__SOCKS5PROXY_ENABLED__=${SOCKS5PROXY_ENABLED:=false}
__SOCKS5PROXY_HOST__=${SOCKS5PROXY_HOST:=localhost}
__SOCKS5PROXY_PORT__=${SOCKS5PROXY_PORT:=9050}
__SOCKS5PROXY_USERNAME__=${SOCKS5PROXY_USERNAME:=""}
__SOCKS5PROXY_PASSWORD__=${SOCKS5PROXY_PASSWORD:=""}
# PRICE_DATA_SERVER
__PRICE_DATA_SERVER_TOR_URL__=${PRICE_DATA_SERVER_TOR_URL:=http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices}
__PRICE_DATA_SERVER_CLEARNET_URL__=${PRICE_DATA_SERVER_CLEARNET_URL:=https://price.bisq.wiz.biz/getAllMarketPrices}
mkdir -p "${__MEMPOOL_CACHE_DIR__}" mkdir -p "${__MEMPOOL_CACHE_DIR__}"
sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json
@ -78,7 +90,8 @@ sed -i "s/__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__/${__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__}
sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}/g" mempool-config.json sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}/g" mempool-config.json
sed -i "s/__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__/${__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__}/g" mempool-config.json sed -i "s/__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__/${__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__}/g" mempool-config.json
sed -i "s/__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__/${__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__}/g" mempool-config.json sed -i "s/__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__/${__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__}/g" mempool-config.json
sed -i "s/__MEMPOOL_EXTERNAL_ASSETS__/${__MEMPOOL_EXTERNAL_ASSETS__}/g" mempool-config.json sed -i "s!__MEMPOOL_EXTERNAL_ASSETS__!${__MEMPOOL_EXTERNAL_ASSETS__}!g" mempool-config.json
sed -i "s/__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__/${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}/g" mempool-config.json
sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json
sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json
@ -115,4 +128,13 @@ sed -i "s/__STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__/${__STATISTICS_TX_PER_SECON
sed -i "s/__BISQ_ENABLED__/${__BISQ_ENABLED__}/g" mempool-config.json sed -i "s/__BISQ_ENABLED__/${__BISQ_ENABLED__}/g" mempool-config.json
sed -i "s!__BISQ_DATA_PATH__!${__BISQ_DATA_PATH__}!g" mempool-config.json sed -i "s!__BISQ_DATA_PATH__!${__BISQ_DATA_PATH__}!g" mempool-config.json
sed -i "s/__SOCKS5PROXY_ENABLED__/${__SOCKS5PROXY_ENABLED__}/g" mempool-config.json
sed -i "s/__SOCKS5PROXY_HOST__/${__SOCKS5PROXY_HOST__}/g" mempool-config.json
sed -i "s/__SOCKS5PROXY_PORT__/${__SOCKS5PROXY_PORT__}/g" mempool-config.json
sed -i "s/__SOCKS5PROXY_USERNAME__/${__SOCKS5PROXY_USERNAME__}/g" mempool-config.json
sed -i "s/__SOCKS5PROXY_PASSWORD__/${__SOCKS5PROXY_PASSWORD__}/g" mempool-config.json
sed -i "s!__PRICE_DATA_SERVER_TOR_URL__!${__PRICE_DATA_SERVER_TOR_URL__}!g" mempool-config.json
sed -i "s!__PRICE_DATA_SERVER_CLEARNET_URL__!${__PRICE_DATA_SERVER_CLEARNET_URL__}!g" mempool-config.json
node /backend/dist/index.js node /backend/dist/index.js

View file

@ -218,6 +218,10 @@
"proxyConfig": "proxy.conf.local.js", "proxyConfig": "proxy.conf.local.js",
"verbose": true "verbose": true
}, },
"mixed": {
"proxyConfig": "proxy.conf.mixed.js",
"verbose": true
},
"staging": { "staging": {
"proxyConfig": "proxy.conf.js", "proxyConfig": "proxy.conf.js",
"disableHostCheck": true, "disableHostCheck": true,
@ -229,6 +233,12 @@
"disableHostCheck": true, "disableHostCheck": true,
"host": "0.0.0.0", "host": "0.0.0.0",
"verbose": false "verbose": false
},
"local-staging": {
"proxyConfig": "proxy.conf.staging.js",
"disableHostCheck": true,
"host": "0.0.0.0",
"verbose": false
} }
} }
}, },

View file

@ -115,17 +115,16 @@ describe('Liquid', () => {
describe('assets', () => { describe('assets', () => {
it('shows the assets screen', () => { it('shows the assets screen', () => {
cy.visit(`${basePath}`); cy.visit(`${basePath}/assets`);
cy.get('#btn-assets');
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('table tr').should('have.length.at.least', 5); cy.get('.featuredBox .card').should('have.length.at.least', 5);
}); });
it('allows searching assets', () => { it('allows searching assets', () => {
cy.visit(`${basePath}/assets`); cy.visit(`${basePath}/assets`);
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('.container-xl input').click().type('Liquid Bitcoin').then(() => { cy.get('.container-xl input').click().type('Liquid Bitcoin').then(() => {
cy.get('table tr').should('have.length', 1); cy.get('ngb-typeahead-window').should('have.length', 1);
}); });
}); });
@ -133,7 +132,7 @@ describe('Liquid', () => {
cy.visit(`${basePath}/assets`); cy.visit(`${basePath}/assets`);
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('.container-xl input').click().type('Liquid AUD').then(() => { cy.get('.container-xl input').click().type('Liquid AUD').then(() => {
cy.get('table tr td:nth-of-type(1) a').click(); cy.get('ngb-typeahead-window:nth-of-type(1) button').click();
}); });
}); });
}); });
@ -197,7 +196,7 @@ describe('Liquid', () => {
}); });
it('shows asset peg in/out and burn transactions', () => { it('shows asset peg in/out and burn transactions', () => {
cy.visit(`${basePath}/asset/6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d`); cy.visit(`${basePath}/assets/asset/6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d`);
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('#table-tx-vout tr').not('.assetBox'); cy.get('#table-tx-vout tr').not('.assetBox');
cy.get('#table-tx-vin tr').not('.assetBox'); cy.get('#table-tx-vin tr').not('.assetBox');

View file

@ -73,17 +73,11 @@ describe('Liquid Testnet', () => {
}); });
describe('assets', () => { describe('assets', () => {
it('shows the assets screen', () => {
cy.visit(`${basePath}/assets`);
cy.waitForSkeletonGone();
cy.get('table tr').should('have.length.at.least', 5);
});
it('allows searching assets', () => { it('allows searching assets', () => {
cy.visit(`${basePath}/assets`); cy.visit(`${basePath}/assets`);
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('.container-xl input').click().type('Liquid Bitcoin').then(() => { cy.get('.container-xl input').click().type('Liquid Bitcoin').then(() => {
cy.get('table tr').should('have.length', 1); cy.get('ngb-typeahead-window').should('have.length', 1);
}); });
}); });
@ -91,7 +85,7 @@ describe('Liquid Testnet', () => {
cy.visit(`${basePath}/assets`); cy.visit(`${basePath}/assets`);
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('.container-xl input').click().type('Liquid CAD').then(() => { cy.get('.container-xl input').click().type('Liquid CAD').then(() => {
cy.get('table tr td:nth-of-type(1) a').click(); cy.get('ngb-typeahead-window:nth-of-type(1) button').click();
}); });
}); });
}); });
@ -150,7 +144,7 @@ describe('Liquid Testnet', () => {
cy.visit(`${basePath}/tx/0877bc0c7aa5c2b8d0e4b15450425879b8783c40e341806037a605ef836fb886#blinded=5000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,328de54e90e867a9154b4f1eb7fcab86267e880fa2ee9e53b41a91e61dab86e6,8885831e6b089eaf06889d53a24843f0da533d300a7b1527b136883a6819f3ae,5000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,aca78b953615d69ae0ae68c4c5c3c0ee077c10bc20ad3f0c5960706004e6cb56,d2ec175afe5f761e2dbd443faf46abbb7091f341deb3387e5787d812bdb2df9f,100000,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,4b54a4ca809b3844f34dd88b68617c4c866d92a02211f02ba355755bac20a1c6,eddd02e92b0cfbad8cab89828570a50f2c643bb2a54d886c86e25ce47e818685,99729,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,8b86d565c9549eb0352bb81ee576d01d064435b64fddcc045decebeb1d9913ce,b082ce3448d40d47b5b39f15d72b285f4a1046b636b56c25f32f498ece29d062,10000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,62b04ee86198d6b41681cdd0acb450ab366af727a010aaee8ba0b9e69ff43896,3f98429bca9b538dc943c22111f25d9c4448d45a63ff0f4e58b22fd434c0365e`); cy.visit(`${basePath}/tx/0877bc0c7aa5c2b8d0e4b15450425879b8783c40e341806037a605ef836fb886#blinded=5000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,328de54e90e867a9154b4f1eb7fcab86267e880fa2ee9e53b41a91e61dab86e6,8885831e6b089eaf06889d53a24843f0da533d300a7b1527b136883a6819f3ae,5000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,aca78b953615d69ae0ae68c4c5c3c0ee077c10bc20ad3f0c5960706004e6cb56,d2ec175afe5f761e2dbd443faf46abbb7091f341deb3387e5787d812bdb2df9f,100000,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,4b54a4ca809b3844f34dd88b68617c4c866d92a02211f02ba355755bac20a1c6,eddd02e92b0cfbad8cab89828570a50f2c643bb2a54d886c86e25ce47e818685,99729,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,8b86d565c9549eb0352bb81ee576d01d064435b64fddcc045decebeb1d9913ce,b082ce3448d40d47b5b39f15d72b285f4a1046b636b56c25f32f498ece29d062,10000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,62b04ee86198d6b41681cdd0acb450ab366af727a010aaee8ba0b9e69ff43896,3f98429bca9b538dc943c22111f25d9c4448d45a63ff0f4e58b22fd434c0365e`);
cy.get('#table-tx-vout tr:nth-child(2) .amount a').click().then(() => { cy.get('#table-tx-vout tr:nth-child(2) .amount a').click().then(() => {
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.url().should('contain', '/asset/38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5'); cy.url().should('contain', '/assets/asset/38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5');
}); });
}); });
@ -162,7 +156,7 @@ describe('Liquid Testnet', () => {
}); });
it('shows asset peg in/out and burn transactions', () => { it('shows asset peg in/out and burn transactions', () => {
cy.visit(`${basePath}/asset/ac3e0ff248c5051ffd61e00155b7122e5ebc04fd397a0ecbdd4f4e4a56232926`); cy.visit(`${basePath}/assets/asset/ac3e0ff248c5051ffd61e00155b7122e5ebc04fd397a0ecbdd4f4e4a56232926`);
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('#table-tx-vout tr').not('.assetBox'); cy.get('#table-tx-vout tr').not('.assetBox');
cy.get('#table-tx-vin tr').not('.assetBox'); cy.get('#table-tx-vin tr').not('.assetBox');

View file

@ -27,9 +27,12 @@
"serve": "npm run generate-config && ng serve -c local", "serve": "npm run generate-config && ng serve -c local",
"serve:stg": "npm run generate-config && ng serve -c staging", "serve:stg": "npm run generate-config && ng serve -c staging",
"serve:local-prod": "npm run generate-config && ng serve -c local-prod", "serve:local-prod": "npm run generate-config && ng serve -c local-prod",
"serve:local-staging": "npm run generate-config && ng serve -c local-staging",
"start": "npm run generate-config && npm run sync-assets-dev && ng serve -c local", "start": "npm run generate-config && npm run sync-assets-dev && ng serve -c local",
"start:stg": "npm run generate-config && npm run sync-assets-dev && ng serve -c staging", "start:stg": "npm run generate-config && npm run sync-assets-dev && ng serve -c staging",
"start:local-prod": "npm run generate-config && npm run sync-assets-dev && ng serve -c local-prod", "start:local-prod": "npm run generate-config && npm run sync-assets-dev && ng serve -c local-prod",
"start:local-staging": "npm run generate-config && npm run sync-assets-dev && ng serve -c local-staging",
"start:mixed": "npm run generate-config && npm run sync-assets-dev && ng serve -c mixed",
"build": "npm run generate-config && ng build --configuration production --localize && npm run sync-assets && npm run build-mempool.js", "build": "npm run generate-config && ng build --configuration production --localize && npm run sync-assets && npm run build-mempool.js",
"sync-assets": "node sync-assets.js && rsync -av ./dist/mempool/browser/en-US/resources ./dist/mempool/browser/resources", "sync-assets": "node sync-assets.js && rsync -av ./dist/mempool/browser/en-US/resources ./dist/mempool/browser/resources",
"sync-assets-dev": "node sync-assets.js dev", "sync-assets-dev": "node sync-assets.js dev",
@ -53,7 +56,10 @@
"cypress:run": "cypress run", "cypress:run": "cypress run",
"cypress:run:record": "cypress run --record", "cypress:run:record": "cypress run --record",
"cypress:open:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:open", "cypress:open:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:open",
"cypress:run:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record" "cypress:run:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record",
"cypress:open:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:open",
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
}, },
"dependencies": { "dependencies": {
"@angular-devkit/build-angular": "^13.1.2", "@angular-devkit/build-angular": "^13.1.2",

View file

@ -20,8 +20,8 @@ try {
PROXY_CONFIG = [ PROXY_CONFIG = [
{ {
context: ['*', context: ['*',
'/api/**', '!/api/v1/ws', '/api/**', '!/api/v1/ws',
'!/bisq', '!/bisq/**', '!/bisq/', '!/bisq', '!/bisq/**', '!/bisq/',
'!/liquid', '!/liquid/**', '!/liquid/', '!/liquid', '!/liquid/**', '!/liquid/',
'!/liquidtestnet', '!/liquidtestnet/**', '!/liquidtestnet/', '!/liquidtestnet', '!/liquidtestnet/**', '!/liquidtestnet/',
@ -65,7 +65,13 @@ PROXY_CONFIG = [
ws: true, ws: true,
secure: false, secure: false,
changeOrigin: true changeOrigin: true
} },
{
context: ['/resources/mining-pools/**'],
target: "https://mempool.space",
secure: false,
changeOrigin: true
}
]; ];
if (configContent && configContent.BASE_MODULE == "liquid") { if (configContent && configContent.BASE_MODULE == "liquid") {

View file

@ -1,17 +1,13 @@
const fs = require('fs'); const fs = require('fs');
let PROXY_CONFIG = require('./proxy.conf.js');
const BACKEND_CONFIG_FILE_NAME = '../backend/mempool-config.json';
const FRONTEND_CONFIG_FILE_NAME = 'mempool-frontend-config.json'; const FRONTEND_CONFIG_FILE_NAME = 'mempool-frontend-config.json';
let backendConfigContent; let configContent;
let frontendConfigContent;
// Read frontend config // Read frontend config
try { try {
const rawConfig = fs.readFileSync(FRONTEND_CONFIG_FILE_NAME); const rawConfig = fs.readFileSync(FRONTEND_CONFIG_FILE_NAME);
frontendConfigContent = JSON.parse(rawConfig); configContent = JSON.parse(rawConfig);
console.log(`${FRONTEND_CONFIG_FILE_NAME} file found, using provided config`); console.log(`${FRONTEND_CONFIG_FILE_NAME} file found, using provided config`);
} catch (e) { } catch (e) {
console.log(e); console.log(e);
@ -22,50 +18,88 @@ try {
} }
} }
// Read backend config let PROXY_CONFIG = [];
try {
const rawConfig = fs.readFileSync(BACKEND_CONFIG_FILE_NAME); if (configContent && configContent.BASE_MODULE === 'liquid') {
backendConfigContent = JSON.parse(rawConfig); PROXY_CONFIG.push(...[
console.log(`${BACKEND_CONFIG_FILE_NAME} file found, using provided config`); {
} catch (e) { context: ['/liquid/api/v1/**'],
console.log(e); target: `http://localhost:8999`,
if (e.code !== 'ENOENT') { secure: false,
throw new Error(e); ws: true,
} else { changeOrigin: true,
console.log(`${BACKEND_CONFIG_FILE_NAME} file not found, using default config`); proxyTimeout: 30000,
pathRewrite: {
"^/liquid": ""
},
},
{
context: ['/liquid/api/**'],
target: `http://localhost:8999`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/liquid/api/": "/api/v1/"
},
}
]);
}
if (configContent && configContent.BASE_MODULE === 'bisq') {
PROXY_CONFIG.push(...[
{
context: ['/bisq/api/v1/ws'],
target: `http://localhost:8999`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/bisq": ""
},
},
{
context: ['/bisq/api/v1/**'],
target: `http://localhost:8999`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
},
{
context: ['/bisq/api/**'],
target: `http://localhost:8999`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/bisq/api/": "/api/v1/bisq/"
},
}
]);
}
PROXY_CONFIG.push(...[
{
context: ['/api/v1/**'],
target: `http://localhost:8999`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
},
{
context: ['/api/**'],
target: `http://localhost:8999`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/api/": "/api/v1/"
},
} }
} ]);
// Remove the "/api/**" entry from the default proxy config
let localDevContext = PROXY_CONFIG[0].context
localDevContext.splice(PROXY_CONFIG[0].context.indexOf('/api/**'), 1);
PROXY_CONFIG[0].context = localDevContext;
// Change all targets to localhost
PROXY_CONFIG.map(conf => conf.target = "http://localhost:8999");
// Add rules for local backend
if (backendConfigContent) {
PROXY_CONFIG.push({
context: ['/api/v1/**'],
target: `http://localhost:8999`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000
});
PROXY_CONFIG.push({
context: ['/api/**'],
target: `http://localhost:8999`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/api/": "/api/v1/"
},
});
}
console.log(PROXY_CONFIG); console.log(PROXY_CONFIG);

View file

@ -0,0 +1,99 @@
const fs = require('fs');
const FRONTEND_CONFIG_FILE_NAME = 'mempool-frontend-config.json';
let configContent;
// Read frontend config
try {
const rawConfig = fs.readFileSync(FRONTEND_CONFIG_FILE_NAME);
configContent = JSON.parse(rawConfig);
console.log(`${FRONTEND_CONFIG_FILE_NAME} file found, using provided config`);
} catch (e) {
console.log(e);
if (e.code !== 'ENOENT') {
throw new Error(e);
} else {
console.log(`${FRONTEND_CONFIG_FILE_NAME} file not found, using default config`);
}
}
let PROXY_CONFIG = [];
if (configContent && configContent.BASE_MODULE === 'liquid') {
PROXY_CONFIG.push(...[
{
context: ['/liquid/api/v1/**'],
target: `http://localhost:8999`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/liquid": ""
},
},
{
context: ['/liquid/api/**'],
target: `https://liquid.network`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
},
]);
}
if (configContent && configContent.BASE_MODULE === 'bisq') {
PROXY_CONFIG.push(...[
{
context: ['/bisq/api/v1/ws'],
target: `http://localhost:8999`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/bisq": ""
},
},
{
context: ['/bisq/api/v1/**'],
target: `http://localhost:8999`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
},
{
context: ['/bisq/api/**'],
target: `http://localhost:8999`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/bisq/api/": "/api/v1/bisq/"
},
},
]);
}
PROXY_CONFIG.push(...[
{
context: ['/api/v1/**'],
target: `http://localhost:8999`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
},
{
context: ['/api/**'],
target: `https://mempool.space`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
}
]);
console.log(PROXY_CONFIG);
module.exports = PROXY_CONFIG;

View file

@ -0,0 +1,11 @@
const fs = require('fs');
let PROXY_CONFIG = require('./proxy.conf');
PROXY_CONFIG.forEach(entry => {
entry.target = entry.target.replace("mempool.space", "mempool.ninja");
entry.target = entry.target.replace("liquid.network", "liquid.place");
entry.target = entry.target.replace("bisq.markets", "bisq.ninja");
});
module.exports = PROXY_CONFIG;

View file

@ -10,7 +10,7 @@ import { TelevisionComponent } from './components/television/television.componen
import { StatisticsComponent } from './components/statistics/statistics.component'; import { StatisticsComponent } from './components/statistics/statistics.component';
import { MempoolBlockComponent } from './components/mempool-block/mempool-block.component'; import { MempoolBlockComponent } from './components/mempool-block/mempool-block.component';
import { AssetComponent } from './components/asset/asset.component'; import { AssetComponent } from './components/asset/asset.component';
import { AssetsComponent } from './assets/assets.component'; import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component';
import { StatusViewComponent } from './components/status-view/status-view.component'; import { StatusViewComponent } from './components/status-view/status-view.component';
import { DashboardComponent } from './dashboard/dashboard.component'; import { DashboardComponent } from './dashboard/dashboard.component';
import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component'; import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component';
@ -23,6 +23,9 @@ import { SponsorComponent } from './components/sponsor/sponsor.component';
import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component'; import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component';
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component'; import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
import { PoolRankingComponent } from './components/pool-ranking/pool-ranking.component'; import { PoolRankingComponent } from './components/pool-ranking/pool-ranking.component';
import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component';
import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component';
import { AssetsComponent } from './components/assets/assets.component';
let routes: Routes = [ let routes: Routes = [
{ {
@ -343,13 +346,31 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
path: 'address/:id', path: 'address/:id',
component: AddressComponent component: AddressComponent
}, },
{
path: 'asset/:id',
component: AssetComponent
},
{ {
path: 'assets', path: 'assets',
component: AssetsComponent, component: AssetsNavComponent,
children: [
{
path: 'featured',
component: AssetsFeaturedComponent,
},
{
path: 'all',
component: AssetsComponent,
},
{
path: 'asset/:id',
component: AssetComponent
},
{
path: 'group/:id',
component: AssetGroupComponent
},
{
path: '**',
redirectTo: 'featured'
}
]
}, },
{ {
path: 'docs/api/:type', path: 'docs/api/:type',
@ -434,13 +455,27 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
path: 'address/:id', path: 'address/:id',
component: AddressComponent component: AddressComponent
}, },
{
path: 'asset/:id',
component: AssetComponent
},
{ {
path: 'assets', path: 'assets',
component: AssetsComponent, component: AssetsNavComponent,
children: [
{
path: 'all',
component: AssetsComponent,
},
{
path: 'asset/:id',
component: AssetComponent
},
{
path: 'group/:id',
component: AssetGroupComponent
},
{
path: '**',
redirectTo: 'all'
}
]
}, },
{ {
path: 'docs/api/:type', path: 'docs/api/:type',

View file

@ -40,7 +40,8 @@ import { MempoolGraphComponent } from './components/mempool-graph/mempool-graph.
import { PoolRankingComponent } from './components/pool-ranking/pool-ranking.component'; import { PoolRankingComponent } from './components/pool-ranking/pool-ranking.component';
import { LbtcPegsGraphComponent } from './components/lbtc-pegs-graph/lbtc-pegs-graph.component'; import { LbtcPegsGraphComponent } from './components/lbtc-pegs-graph/lbtc-pegs-graph.component';
import { AssetComponent } from './components/asset/asset.component'; import { AssetComponent } from './components/asset/asset.component';
import { AssetsComponent } from './assets/assets.component'; import { AssetsComponent } from './components/assets/assets.component';
import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component';
import { StatusViewComponent } from './components/status-view/status-view.component'; import { StatusViewComponent } from './components/status-view/status-view.component';
import { MinerComponent } from './components/miner/miner.component'; import { MinerComponent } from './components/miner/miner.component';
import { SharedModule } from './shared/shared.module'; import { SharedModule } from './shared/shared.module';
@ -64,6 +65,8 @@ import { LanguageService } from './services/language.service';
import { SponsorComponent } from './components/sponsor/sponsor.component'; import { SponsorComponent } from './components/sponsor/sponsor.component';
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component'; import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component';
import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -110,6 +113,9 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
PushTransactionComponent, PushTransactionComponent,
DocsComponent, DocsComponent,
ApiDocsNavComponent, ApiDocsNavComponent,
AssetsNavComponent,
AssetsFeaturedComponent,
AssetGroupComponent,
], ],
imports: [ imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }), BrowserModule.withServerTransition({ appId: 'serverApp' }),

View file

@ -1,71 +0,0 @@
<div class="container-xl">
<div class="title-asset">
<h1 i18n="Registered assets page header">Registered assets</h1>
</div>
<div class="clearfix"></div>
<form [formGroup]="searchForm" class="form-inline">
<div class="input-group mb-2">
<input style="width: 250px;" formControlName="searchText" type="text" class="form-control" i18n-placeholder="Search Assets Placeholder Text" placeholder="Search asset">
<div class="input-group-append">
<button [disabled]="!searchForm.get('searchText')?.value.length" class="btn btn-secondary" type="button" (click)="searchForm.get('searchText')?.setValue('');" autocomplete="off" i18n="Search Clear Button">Clear</button>
</div>
</div>
</form>
<ng-container *ngIf="(assets$ | async) as filteredAssets; else isLoading">
<table class="table table-borderless table-striped">
<thead>
<th class="td-name" i18n="Asset name header">Name</th>
<th i18n="Asset ticker header">Ticker</th>
<th class="d-none d-md-block" i18n="Asset Issuer Domain header">Issuer domain</th>
<th i18n="Asset ID header">Asset ID</th>
</thead>
<tbody>
<tr *ngFor="let asset of filteredAssets; trackBy: trackByAsset">
<td class="td-name"><a [routerLink]="['/asset/' | relativeUrl, asset.asset_id]">{{ asset.name }}</a></td>
<td>{{ asset.ticker }}</td>
<td class="d-none d-md-block">{{ asset.entity && asset.entity.domain }}</td>
<td><a [routerLink]="['/asset/' | relativeUrl, asset.asset_id]">{{ asset.asset_id | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="asset.asset_id"></app-clipboard></td>
</tr>
</tbody>
</table>
<br>
<ngb-pagination [collectionSize]="assets.length" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="5" [boundaryLinks]="true"></ngb-pagination>
</ng-container>
<ng-template #isLoading>
<table class="table table-borderless table-striped">
<thead>
<th i18n="Asset name header">Name</th>
<th i18n="Asset ticker header">Ticker</th>
<th class="d-none d-md-block" i18n="Asset Issuer Domain header">Issuer domain</th>
<th i18n="Asset ID header">Asset ID</th>
</thead>
<tbody>
<tr *ngFor="let dummy of [0,0,0,0,0,0,0,0,0,0]">
<td><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
<td class="d-none d-md-block"><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</ng-template>
<ng-template [ngIf]="error">
<div class="text-center">
<ng-container i18n="Asset data load error">Error loading assets data.</ng-container>
<br>
<i>{{ error.error }}</i>
</div>
</ng-template>
</div>
<br>

View file

@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AssetsComponent } from './assets.component';
describe('AssetsComponent', () => {
let component: AssetsComponent;
let fixture: ComponentFixture<AssetsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AssetsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AssetsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -1,168 +0,0 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { AssetsService } from '../services/assets.service';
import { environment } from 'src/environments/environment';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { distinctUntilChanged, map, filter, mergeMap, tap, take } from 'rxjs/operators';
import { ActivatedRoute, Router } from '@angular/router';
import { merge, combineLatest, Observable } from 'rxjs';
import { AssetExtended } from '../interfaces/electrs.interface';
import { SeoService } from '../services/seo.service';
import { StateService } from '../services/state.service';
@Component({
selector: 'app-assets',
templateUrl: './assets.component.html',
styleUrls: ['./assets.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AssetsComponent implements OnInit {
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
assets: AssetExtended[];
assetsCache: AssetExtended[];
searchForm: FormGroup;
assets$: Observable<AssetExtended[]>;
error: any;
page = 1;
itemsPerPage: number;
contentSpace = window.innerHeight - (250 + 200);
fiveItemsPxSize = 250;
constructor(
private assetsService: AssetsService,
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private router: Router,
private seoService: SeoService,
private stateService: StateService,
) { }
ngOnInit() {
this.seoService.setTitle($localize`:@@ee8f8008bae6ce3a49840c4e1d39b4af23d4c263:Assets`);
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
this.searchForm = this.formBuilder.group({
searchText: [{ value: '', disabled: true }, Validators.required]
});
this.assets$ = combineLatest([
this.assetsService.getAssetsJson$,
this.route.queryParams
])
.pipe(
take(1),
mergeMap(([assets, qp]) => {
this.assets = Object.values(assets);
if (this.stateService.network === 'liquid') {
// @ts-ignore
this.assets.push({
name: 'Liquid Bitcoin',
ticker: 'L-BTC',
asset_id: this.nativeAssetId,
});
} else if (this.stateService.network === 'liquidtestnet') {
// @ts-ignore
this.assets.push({
name: 'Test Liquid Bitcoin',
ticker: 'tL-BTC',
asset_id: this.nativeAssetId,
});
}
this.assets = this.assets.sort((a: any, b: any) => a.name.localeCompare(b.name));
this.assetsCache = this.assets;
this.searchForm.get('searchText').enable();
if (qp.search) {
this.searchForm.get('searchText').setValue(qp.search, { emitEvent: false });
}
return merge(
this.searchForm.get('searchText').valueChanges
.pipe(
distinctUntilChanged(),
tap((text) => {
this.page = 1;
this.searchTextChanged(text);
})
),
this.route.queryParams
.pipe(
filter((queryParams) => {
const newPage = parseInt(queryParams.page, 10);
if (newPage !== this.page || queryParams.search !== this.searchForm.get('searchText').value) {
return true;
}
return false;
}),
map((queryParams) => {
if (queryParams.page) {
const newPage = parseInt(queryParams.page, 10);
this.page = newPage;
} else {
this.page = 1;
}
if (this.searchForm.get('searchText').value !== (queryParams.search || '')) {
this.searchTextChanged(queryParams.search);
}
if (queryParams.search) {
this.searchForm.get('searchText').setValue(queryParams.search, { emitEvent: false });
return queryParams.search;
}
return '';
})
),
);
}),
map((searchText) => {
const start = (this.page - 1) * this.itemsPerPage;
if (searchText.length ) {
const filteredAssets = this.assetsCache.filter((asset) => asset.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1
|| (asset.ticker || '').toLowerCase().indexOf(searchText.toLowerCase()) > -1);
this.assets = filteredAssets;
return filteredAssets.slice(start, this.itemsPerPage + start);
} else {
this.assets = this.assetsCache;
return this.assets.slice(start, this.itemsPerPage + start);
}
})
);
}
pageChange(page: number) {
const queryParams = { page: page, search: this.searchForm.get('searchText').value };
if (queryParams.search === '') {
queryParams.search = null;
}
if (queryParams.page === 1) {
queryParams.page = null;
}
this.page = -1;
this.router.navigate([], {
relativeTo: this.route,
queryParams: queryParams,
queryParamsHandling: 'merge',
});
}
searchTextChanged(text: string) {
const queryParams = { search: text, page: 1 };
if (queryParams.search === '') {
queryParams.search = null;
}
if (queryParams.page === 1) {
queryParams.page = null;
}
this.router.navigate([], {
relativeTo: this.route,
queryParams: queryParams,
queryParamsHandling: 'merge',
});
}
trackByAsset(index: number, asset: any) {
return asset.asset_id;
}
}

View file

@ -2,7 +2,7 @@
<div class="title-asset"> <div class="title-asset">
<h1 i18n="asset|Liquid Asset page title">Asset</h1> <h1 i18n="asset|Liquid Asset page title">Asset</h1>
<div class="tx-link"> <div class="tx-link">
<a [routerLink]="['/asset/' | relativeUrl, assetString]"> <a [routerLink]="['/assets/asset/' | relativeUrl, assetString]">
<span class="d-inline d-lg-none">{{ assetString | shortenString : 24 }}</span> <span class="d-inline d-lg-none">{{ assetString | shortenString : 24 }}</span>
<span class="d-none d-lg-inline">{{ assetString }}</span> <span class="d-none d-lg-inline">{{ assetString }}</span>
</a> </a>
@ -20,7 +20,7 @@
<table class="table table-borderless table-striped"> <table class="table table-borderless table-striped">
<tbody> <tbody>
<tr> <tr>
<td i18n="asset.name|Liquid Asset name">Name</td> <td i18n="Asset name header">Name</td>
<td class="assetName">{{ assetContract[2] }} ({{ assetContract[1] }})</td> <td class="assetName">{{ assetContract[2] }} ({{ assetContract[1] }})</td>
</tr> </tr>
<tr> <tr>

View file

@ -63,6 +63,7 @@ export class AssetComponent implements OnInit, OnDestroy {
.pipe( .pipe(
switchMap((params: ParamMap) => { switchMap((params: ParamMap) => {
this.error = undefined; this.error = undefined;
this.imageError = false;
this.isLoadingAsset = true; this.isLoadingAsset = true;
this.loadedConfirmedTxCount = 0; this.loadedConfirmedTxCount = 0;
this.asset = null; this.asset = null;

View file

@ -0,0 +1,35 @@
<div *ngIf="group$ | async as group; else loading">
<div class="main-title">
<h2>{{ group.group.name }}</h2>
<div class="sub-title" i18n>Group of {{ group.group.assets.length | number }} assets</div>
</div>
<div class="clearfix"></div>
<br>
<div class="featuredBox">
<div *ngFor="let asset of group.assets">
<div class="card">
<a [routerLink]="['/assets/asset' | relativeUrl, asset.asset_id]">
<img class="assetIcon" [src]="'https://liquid.network/api/v1/asset/' + asset.asset_id + '/icon'">
</a>
<div class="title">
<a [routerLink]="['/assets/asset/' | relativeUrl, asset.asset_id]">{{ asset.name }}</a>
</div>
<div class="ticker">{{ asset.ticker }}</div>
</div>
</div>
</div>
</div>
<ng-template #loading>
<br>
<div class="text-center loadingGraphs">
<div class="spinner-border text-light"></div>
</div>
</ng-template>

View file

@ -0,0 +1,60 @@
.image {
width: 150px;
float: left;
}
.main-title {
float: left
}
.sub-title {
color: grey;
}
.featuredBox {
display: flex;
flex-flow: row wrap;
justify-content: center;
gap: 27px;
}
.card {
background-color: #1d1f31;
width: 200px;
height: 200px;
align-items: center;
justify-content: center;
flex-wrap: wrap;
@media (max-width: 767.98px) {
width: 150px;
height: 150px;
}
}
.title {
font-size: 14px;
font-weight: bold;
margin-top: 10px;
text-align: center;
}
.sub-title {
color: grey;
}
.assetIcon {
width: 100px;
height: 100px;
@media (max-width: 767.98px) {
width: 50px;
height: 50px;
}
}
.view-link {
margin-top: 30px;
}
.ticker {
color: grey;
}

View file

@ -0,0 +1,44 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { combineLatest, Observable } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { ApiService } from 'src/app/services/api.service';
import { AssetsService } from 'src/app/services/assets.service';
@Component({
selector: 'app-asset-group',
templateUrl: './asset-group.component.html',
styleUrls: ['./asset-group.component.scss']
})
export class AssetGroupComponent implements OnInit {
group$: Observable<any>;
constructor(
private route: ActivatedRoute,
private apiService: ApiService,
private assetsService: AssetsService,
) { }
ngOnInit(): void {
this.group$ = this.route.paramMap
.pipe(
switchMap((params: ParamMap) => {
return combineLatest([
this.assetsService.getAssetsJson$,
this.apiService.getAssetGroup$(params.get('id')),
]);
}),
map(([assets, group]) => {
const items = [];
// @ts-ignore
for (const item of group.assets) {
items.push(assets.objects[item]);
}
return {
group: group,
assets: items
};
})
);
}
}

View file

@ -0,0 +1,29 @@
<div *ngIf="featuredAssets$ | async as featured; else loading" class="featuredBox">
<div class="card" *ngFor="let group of featured">
<ng-template [ngIf]="group.assets" [ngIfElse]="singleAsset">
<a [routerLink]="['/assets/group' | relativeUrl, group.id]">
<img class="assetIcon" [src]="'https://liquid.network/api/v1/asset/' + group.assets[0] + '/icon'">
</a>
<div class="title"><a [routerLink]="['/assets/group' | relativeUrl, group.id]">{{ group.name }}</a></div>
<div class="sub-title" i18n>Group of {{ group.assets.length | number }} assets</div>
</ng-template>
<ng-template #singleAsset>
<a [routerLink]="['/assets/asset/' | relativeUrl, group.asset]">
<img class="assetIcon" [src]="'https://liquid.network/api/v1/asset/' + group.asset + '/icon'">
</a>
<div class="title">
<a [routerLink]="['/assets/asset/' | relativeUrl, group.asset]">{{ group.name }}</a>
</div>
<div class="ticker">{{ group.ticker }}</div>
</ng-template>
</div>
</div>
<ng-template #loading>
<br>
<div class="text-center loadingGraphs">
<div class="spinner-border text-light"></div>
</div>
</ng-template>

View file

@ -0,0 +1,49 @@
.featuredBox {
display: flex;
flex-flow: row wrap;
justify-content: center;
gap: 27px;
}
.card {
background-color: #1d1f31;
width: 200px;
height: 200px;
align-items: center;
justify-content: center;
flex-wrap: wrap;
@media (max-width: 767.98px) {
width: 150px;
height: 150px;
}
}
.title {
font-size: 14px;
font-weight: bold;
margin-top: 10px;
text-align: center;
}
.sub-title {
color: grey;
font-size: 12px;
}
.assetIcon {
width: 100px;
height: 100px;
@media (max-width: 767.98px) {
width: 50px;
height: 50px;
}
}
.view-link {
margin-top: 30px;
}
.ticker {
color: grey;
}

View file

@ -0,0 +1,21 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { ApiService } from 'src/app/services/api.service';
@Component({
selector: 'app-assets-featured',
templateUrl: './assets-featured.component.html',
styleUrls: ['./assets-featured.component.scss']
})
export class AssetsFeaturedComponent implements OnInit {
featuredAssets$: Observable<any>;
constructor(
private apiService: ApiService,
) { }
ngOnInit(): void {
this.featuredAssets$ = this.apiService.listFeaturedAssets$();
}
}

View file

@ -0,0 +1,33 @@
<div class="container-xl">
<div class="title-asset">
<h1 i18n="Assets page header">Assets</h1>
</div>
<div class="nav-container">
<ul class="nav nav-pills">
<li class="nav-item">
<a class="nav-link" [routerLink]="['/assets/featured' | relativeUrl]" routerLinkActive="active" i18n>Featured</a>
</li>
<li class="nav-item">
<a class="nav-link" [routerLink]="['/assets/all' | relativeUrl]" routerLinkActive="active" i18n>All</a>
</li>
</ul>
<form [formGroup]="searchForm">
<div class="input-group mb-2">
<input #instance="ngbTypeahead" [ngbTypeahead]="typeaheadSearchFn" [resultFormatter]="formatterFn" (selectItem)="itemSelected()" (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="Search Assets Placeholder Text" placeholder="Search asset">
<div class="input-group-append">
<button [disabled]="!searchForm.get('searchText')?.value.length" class="btn btn-secondary" type="button" (click)="searchForm.get('searchText')?.setValue('');" autocomplete="off" i18n="Search Clear Button">Clear</button>
</div>
</div>
</form>
</div>
<div class="clearfix"></div>
<router-outlet></router-outlet>
</div>
<br>

View file

@ -0,0 +1,24 @@
ul {
margin-bottom: 20px;
float: left;
}
form {
float: right;
width: 300px;
@media (max-width: 767.98px) {
width: 90%;
margin-bottom: 15px;
}
}
@media (max-width: 767.98px) {
.nav-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: auto;
}
}

View file

@ -0,0 +1,95 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
import { merge, Observable, of, Subject } from 'rxjs';
import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
import { AssetExtended } from 'src/app/interfaces/electrs.interface';
import { AssetsService } from 'src/app/services/assets.service';
import { SeoService } from 'src/app/services/seo.service';
import { StateService } from 'src/app/services/state.service';
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
import { environment } from 'src/environments/environment';
@Component({
selector: 'app-assets-nav',
templateUrl: './assets-nav.component.html',
styleUrls: ['./assets-nav.component.scss']
})
export class AssetsNavComponent implements OnInit {
@ViewChild('instance', {static: true}) instance: NgbTypeahead;
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
searchForm: FormGroup;
assetsCache: AssetExtended[];
typeaheadSearchFn: ((text: Observable<string>) => Observable<readonly any[]>);
formatterFn = (asset: AssetExtended) => asset.name + ' (' + asset.ticker + ')';
focus$ = new Subject<string>();
click$ = new Subject<string>();
itemsPerPage = 15;
constructor(
private formBuilder: FormBuilder,
private seoService: SeoService,
private router: Router,
private assetsService: AssetsService,
private stateService: StateService,
private relativeUrlPipe: RelativeUrlPipe,
) { }
ngOnInit(): void {
this.seoService.setTitle($localize`:@@ee8f8008bae6ce3a49840c4e1d39b4af23d4c263:Assets`);
this.typeaheadSearchFn = this.typeaheadSearch;
this.searchForm = this.formBuilder.group({
searchText: [{ value: '', disabled: false }, Validators.required]
});
}
typeaheadSearch = (text$: Observable<string>) => {
const debouncedText$ = text$.pipe(
distinctUntilChanged()
);
const clicksWithClosedPopup$ = this.click$.pipe(filter(() => !this.instance.isPopupOpen()));
const inputFocus$ = this.focus$;
return merge(debouncedText$, inputFocus$, clicksWithClosedPopup$)
.pipe(
switchMap((searchText) => {
if (!searchText.length) {
return of([]);
}
return this.assetsService.getAssetsJson$.pipe(
map((assets) => {
if (searchText.length ) {
const filteredAssets = assets.array.filter((asset) => asset.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1
|| (asset.ticker || '').toLowerCase().indexOf(searchText.toLowerCase()) > -1
|| (asset.entity && asset.entity.domain || '').toLowerCase().indexOf(searchText.toLowerCase()) > -1);
return filteredAssets.slice(0, this.itemsPerPage);
} else {
return assets.array.slice(0, this.itemsPerPage);
}
})
)
}),
);
}
itemSelected() {
setTimeout(() => this.search());
}
search() {
const searchText = this.searchForm.value.searchText;
this.navigate('/assets/asset/', searchText.asset_id);
}
navigate(url: string, searchText: string, extras?: any) {
this.router.navigate([this.relativeUrlPipe.transform(url), searchText], extras);
this.searchForm.setValue({
searchText: '',
});
}
}

View file

@ -0,0 +1,52 @@
<ng-container *ngIf="(assets$ | async) as filteredAssets; else isLoading">
<table class="table table-borderless table-striped">
<thead>
<th class="td-name" i18n="Asset name header">Name</th>
<th i18n="Asset ticker header">Ticker</th>
<th class="d-none d-md-table-cell" i18n="Asset Issuer Domain header">Issuer domain</th>
<th class="d-none d-lg-table-cell" i18n="Asset ID header">Asset ID</th>
</thead>
<tbody>
<tr *ngFor="let asset of filteredAssets; trackBy: trackByAsset">
<td class="td-name"><a [routerLink]="['/assets/asset/' | relativeUrl, asset.asset_id]">{{ asset.name }}</a></td>
<td>{{ asset.ticker }}</td>
<td class="d-none d-md-table-cell">{{ asset.entity && asset.entity.domain }}</td>
<td class="d-none d-lg-table-cell"><a [routerLink]="['/assets/asset/' | relativeUrl, asset.asset_id]">{{ asset.asset_id | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="asset.asset_id"></app-clipboard></td>
</tr>
</tbody>
</table>
<br>
<ngb-pagination [collectionSize]="assets.length" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="ellipses"></ngb-pagination>
</ng-container>
<ng-template #isLoading>
<table class="table table-borderless table-striped">
<thead>
<th i18n="Asset name header">Name</th>
<th i18n="Asset ticker header">Ticker</th>
<th class="d-none d-md-table-cell" i18n="Asset Issuer Domain header">Issuer domain</th>
<th class="d-none d-lg-table-cell" i18n="Asset ID header">Asset ID</th>
</thead>
<tbody>
<tr *ngFor="let dummy of [0,0,0,0,0,0,0,0,0,0]">
<td><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
<td class="d-none d-md-table-cell"><span class="skeleton-loader"></span></td>
<td class="d-none d-lg-table-cell"><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</ng-template>
<ng-template [ngIf]="error">
<div class="text-center">
<ng-container i18n="Asset data load error">Error loading assets data.</ng-container>
<br>
<i>{{ error.error }}</i>
</div>
</ng-template>

View file

@ -0,0 +1,99 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { AssetsService } from 'src/app/services/assets.service';
import { environment } from 'src/environments/environment';
import { FormGroup } from '@angular/forms';
import { filter, map, switchMap, take } from 'rxjs/operators';
import { ActivatedRoute, Router } from '@angular/router';
import { combineLatest, Observable } from 'rxjs';
import { AssetExtended } from 'src/app/interfaces/electrs.interface';
import { SeoService } from 'src/app/services/seo.service';
import { StateService } from 'src/app/services/state.service';
@Component({
selector: 'app-assets',
templateUrl: './assets.component.html',
styleUrls: ['./assets.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AssetsComponent implements OnInit {
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 4 : 6;
ellipses = window.matchMedia('(max-width: 670px)').matches ? false : true;
assets: AssetExtended[];
assetsCache: AssetExtended[];
searchForm: FormGroup;
assets$: Observable<AssetExtended[]>;
page = 1;
error: any;
itemsPerPage: number;
contentSpace = window.innerHeight - (250 + 200);
fiveItemsPxSize = 250;
constructor(
private assetsService: AssetsService,
private route: ActivatedRoute,
private router: Router,
private seoService: SeoService,
private stateService: StateService,
) { }
ngOnInit() {
this.seoService.setTitle($localize`:@@ee8f8008bae6ce3a49840c4e1d39b4af23d4c263:Assets`);
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
this.assets$ = combineLatest([
this.assetsService.getAssetsJson$,
this.route.queryParams,
])
.pipe(
take(1),
switchMap(([assets, qp]) => {
this.assets = assets.array;
return this.route.queryParams
.pipe(
filter((queryParams) => {
const newPage = parseInt(queryParams.page, 10);
if (newPage !== this.page) {
return true;
}
return false;
}),
map((queryParams) => {
if (queryParams.page) {
const newPage = parseInt(queryParams.page, 10);
this.page = newPage;
} else {
this.page = 1;
}
return '';
})
);
}),
map(() => {
const start = (this.page - 1) * this.itemsPerPage;
return this.assets.slice(start, this.itemsPerPage + start);
})
);
}
pageChange(page: number) {
const queryParams = { page: page };
if (queryParams.page === 1) {
queryParams.page = null;
}
this.page = -1;
this.router.navigate([], {
relativeTo: this.route,
queryParams: queryParams,
queryParamsHandling: 'merge',
});
}
trackByAsset(index: number, asset: any) {
return asset.asset_id;
}
}

View file

@ -105,11 +105,11 @@ export class SearchFormComponent implements OnInit {
const matches = this.regexTransaction.exec(searchText); const matches = this.regexTransaction.exec(searchText);
if (this.network === 'liquid' || this.network === 'liquidtestnet') { if (this.network === 'liquid' || this.network === 'liquidtestnet') {
if (this.assets[matches[1]]) { if (this.assets[matches[1]]) {
this.navigate('/asset/', matches[1]); this.navigate('/assets/asset/', matches[1]);
} }
this.electrsApiService.getAsset$(matches[1]) this.electrsApiService.getAsset$(matches[1])
.subscribe( .subscribe(
() => { this.navigate('/asset/', matches[1]); }, () => { this.navigate('/assets/asset/', matches[1]); },
() => { () => {
this.electrsApiService.getBlock$(matches[1]) this.electrsApiService.getBlock$(matches[1])
.subscribe( .subscribe(

View file

@ -274,5 +274,5 @@
<br /> <br />
{{ assetsMinimal[item.asset][0] }} {{ assetsMinimal[item.asset][0] }}
<br /> <br />
<a [routerLink]="['/asset/' | relativeUrl, item.asset]">{{ item.asset | shortenString : 13 }}</a> <a [routerLink]="['/assets/asset/' | relativeUrl, item.asset]">{{ item.asset | shortenString : 13 }}</a>
</ng-template> </ng-template>

View file

@ -117,6 +117,14 @@ export class ApiService {
return this.httpClient.get<LiquidPegs[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs/month'); return this.httpClient.get<LiquidPegs[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs/month');
} }
listFeaturedAssets$(): Observable<any[]> {
return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/assets/featured');
}
getAssetGroup$(id: string): Observable<any> {
return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/assets/group/' + id);
}
postTransaction$(hexPayload: string): Observable<any> { postTransaction$(hexPayload: string): Observable<any> {
return this.httpClient.post<any>(this.apiBaseUrl + this.apiBasePath + '/api/tx', hexPayload, { responseType: 'text' as 'json'}); return this.httpClient.post<any>(this.apiBaseUrl + this.apiBasePath + '/api/tx', hexPayload, { responseType: 'text' as 'json'});
} }

View file

@ -3,12 +3,16 @@ import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, shareReplay, switchMap } from 'rxjs/operators'; import { map, shareReplay, switchMap } from 'rxjs/operators';
import { StateService } from './state.service'; import { StateService } from './state.service';
import { environment } from 'src/environments/environment';
import { AssetExtended } from '../interfaces/electrs.interface';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class AssetsService { export class AssetsService {
getAssetsJson$: Observable<any>; nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
getAssetsJson$: Observable<{ array: AssetExtended[]; objects: any}>;
getAssetsMinimalJson$: Observable<any>; getAssetsMinimalJson$: Observable<any>;
getMiningPools$: Observable<any>; getMiningPools$: Observable<any>;
@ -24,6 +28,30 @@ export class AssetsService {
this.getAssetsJson$ = this.stateService.networkChanged$ this.getAssetsJson$ = this.stateService.networkChanged$
.pipe( .pipe(
switchMap(() => this.httpClient.get(`${apiBaseUrl}/resources/assets${this.stateService.network === 'liquidtestnet' ? '-testnet' : ''}.json`)), switchMap(() => this.httpClient.get(`${apiBaseUrl}/resources/assets${this.stateService.network === 'liquidtestnet' ? '-testnet' : ''}.json`)),
map((rawAssets) => {
const assets: AssetExtended[] = Object.values(rawAssets);
if (this.stateService.network === 'liquid') {
// @ts-ignore
assets.push({
name: 'Liquid Bitcoin',
ticker: 'L-BTC',
asset_id: this.nativeAssetId,
});
} else if (this.stateService.network === 'liquidtestnet') {
// @ts-ignore
assets.push({
name: 'Test Liquid Bitcoin',
ticker: 'tL-BTC',
asset_id: this.nativeAssetId,
});
}
return {
objects: rawAssets,
array: assets.sort((a: any, b: any) => a.name.localeCompare(b.name)),
};
}),
shareReplay(1), shareReplay(1),
); );
this.getAssetsMinimalJson$ = this.stateService.networkChanged$ this.getAssetsMinimalJson$ = this.stateService.networkChanged$

View file

@ -70,3 +70,15 @@ location /api/v1/translators {
proxy_hide_header content-security-policy; proxy_hide_header content-security-policy;
proxy_hide_header x-frame-options; proxy_hide_header x-frame-options;
} }
location /api/v1/assets {
proxy_pass $mempoolSpaceServices;
proxy_cache services;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache_valid 200 10m;
expires 10m;
proxy_hide_header onion-location;
proxy_hide_header strict-transport-security;
proxy_hide_header content-security-policy;
proxy_hide_header x-frame-options;
}