Compare commits
155 Commits
v2.2.0-dev
...
v2.2.0-rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f8b95a17f | ||
|
|
389c1d794c | ||
|
|
fca66f1b9f | ||
|
|
4c7d0cd2e5 | ||
|
|
1016586992 | ||
|
|
38c8f3acb4 | ||
|
|
962023fbc4 | ||
|
|
b4f8bb2f48 | ||
|
|
c26461fada | ||
|
|
1a996e1640 | ||
|
|
c80532b420 | ||
|
|
74c49b9ae7 | ||
|
|
3f03c9c2b6 | ||
|
|
f00e727e68 | ||
|
|
4338dd6c3f | ||
|
|
8385c50605 | ||
|
|
93c4b1caf1 | ||
|
|
49810b6a47 | ||
|
|
28d685a661 | ||
|
|
95d3d0feaf | ||
|
|
cbc5d67f62 | ||
|
|
87575bc0a2 | ||
|
|
8f74ef58f8 | ||
|
|
2475c67d5b | ||
|
|
bf45bf7b39 | ||
|
|
a1f0417997 | ||
|
|
237f265aab | ||
|
|
0087700aa5 | ||
|
|
861344ed6d | ||
|
|
9e343b346a | ||
|
|
e857dbc874 | ||
|
|
a10cd09ba8 | ||
|
|
f30777934f | ||
|
|
4f6bf297bf | ||
|
|
0121052f0b | ||
|
|
1bd0c40c15 | ||
|
|
2ee96cae44 | ||
|
|
28c8d7dba0 | ||
|
|
9b05ecedc6 | ||
|
|
8fbd273733 | ||
|
|
dec8ae2930 | ||
|
|
353b0e8729 | ||
|
|
71bfcea8a6 | ||
|
|
c54c30209e | ||
|
|
abc6b1519e | ||
|
|
4dcda2cf47 | ||
|
|
d055fabfeb | ||
|
|
dbb365f5e3 | ||
|
|
efb5deda43 | ||
|
|
a4cd6450e3 | ||
|
|
edad15da0d | ||
|
|
e70fd0045d | ||
|
|
794bc99cb6 | ||
|
|
cd1ec53af0 | ||
|
|
3e435d1394 | ||
|
|
50b94f8b72 | ||
|
|
f6f5b69487 | ||
|
|
66b27b9dd0 | ||
|
|
71fa2d67cb | ||
|
|
5cd2cfa097 | ||
|
|
cfd13b3655 | ||
|
|
3ffa60db1f | ||
|
|
4442964124 | ||
|
|
cb034020ef | ||
|
|
5aa57d6df9 | ||
|
|
c1a79e3a33 | ||
|
|
bbd21c9401 | ||
|
|
ad22f9cb46 | ||
|
|
939955fb84 | ||
|
|
63e67dba38 | ||
|
|
8a1230623e | ||
|
|
f20c73af7b | ||
|
|
12c99b86b7 | ||
|
|
934dd67384 | ||
|
|
870bd54b38 | ||
|
|
89300dae98 | ||
|
|
482a891cec | ||
|
|
098ab7d3a7 | ||
|
|
147d44d14b | ||
|
|
8ccdf3973c | ||
|
|
c09eb651ef | ||
|
|
ac91d814d6 | ||
|
|
be2f024da1 | ||
|
|
f137f45cef | ||
|
|
90784deacc | ||
|
|
8ed664e3a9 | ||
|
|
17b6916f31 | ||
|
|
b778d96910 | ||
|
|
5b2eb16d1c | ||
|
|
af61357ced | ||
|
|
f281e84396 | ||
|
|
0dc255edf9 | ||
|
|
2f8f3ca2e9 | ||
|
|
39bb93970b | ||
|
|
72d01a0b67 | ||
|
|
0b4da88802 | ||
|
|
d2fe000ad0 | ||
|
|
dcedc8a5ff | ||
|
|
0d03a9e6cc | ||
|
|
24b7acdc60 | ||
|
|
1000f4dd4d | ||
|
|
d5dba9128e | ||
|
|
84b0375c0c | ||
|
|
bf23a6649c | ||
|
|
aea35d4c86 | ||
|
|
52b7efdd53 | ||
|
|
492abad7a6 | ||
|
|
f566eae471 | ||
|
|
2f2be5c64b | ||
|
|
5d1af0a86e | ||
|
|
5cd5280b21 | ||
|
|
3a957ece05 | ||
|
|
3ead05fa51 | ||
|
|
8a838cd4dc | ||
|
|
b05f731332 | ||
|
|
06fd821bf8 | ||
|
|
6dbfcc9d1a | ||
|
|
001bddd529 | ||
|
|
56518b9655 | ||
|
|
da050ee3dc | ||
|
|
5878a2e631 | ||
|
|
c1fc08196b | ||
|
|
95a80157a7 | ||
|
|
165aa6eee2 | ||
|
|
b8fe7b621c | ||
|
|
04ec5e9564 | ||
|
|
2d4dff6de8 | ||
|
|
5cb98b9813 | ||
|
|
d4508bd876 | ||
|
|
6ccac1df79 | ||
|
|
b38fc824e6 | ||
|
|
cdbe90c182 | ||
|
|
6b5b80f866 | ||
|
|
d74677628b | ||
|
|
f0d46d6ed8 | ||
|
|
220d9afd97 | ||
|
|
dfd88a7ff9 | ||
|
|
eec36ae4e6 | ||
|
|
0a07a16650 | ||
|
|
e62ee72149 | ||
|
|
117f5410d7 | ||
|
|
f6ea45b61f | ||
|
|
344d1247bd | ||
|
|
fcf7955d63 | ||
|
|
1ae002385d | ||
|
|
dc36bfcfe4 | ||
|
|
da77dbece1 | ||
|
|
8e29a4cefd | ||
|
|
146fcfc16d | ||
|
|
308dd2c7ad | ||
|
|
1d4ed85d50 | ||
|
|
d99fd5d59a | ||
|
|
2fca34faaa | ||
|
|
38e866995f | ||
|
|
eeb7447988 |
@@ -2,7 +2,7 @@
|
||||
|
||||
Mempool is the fully featured visualizer, explorer, and API service running on [mempool.space](https://mempool.space/), an open source project developed and operated for the benefit of the Bitcoin community, with a focus on the emerging transaction fee market to help our transition into a multi-layer ecosystem.
|
||||
|
||||

|
||||

|
||||
|
||||
## Installation Methods
|
||||
|
||||
|
||||
@@ -50,11 +50,7 @@
|
||||
"ENABLED": true,
|
||||
"TX_PER_SECOND_SAMPLE_PERIOD": 150
|
||||
},
|
||||
"BISQ_BLOCKS": {
|
||||
"ENABLED": false,
|
||||
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db/json"
|
||||
},
|
||||
"BISQ_MARKETS": {
|
||||
"BISQ": {
|
||||
"ENABLED": false,
|
||||
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
|
||||
}
|
||||
|
||||
69
backend/package-lock.json
generated
69
backend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mempool-backend",
|
||||
"version": "2.0.0",
|
||||
"version": "2.2.0-dev",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mempool-backend",
|
||||
"version": "2.0.0",
|
||||
"version": "2.2.0-dev",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"dependencies": {
|
||||
"@mempool/bitcoin": "^3.0.2",
|
||||
@@ -18,13 +18,13 @@
|
||||
"locutus": "^2.0.12",
|
||||
"mysql2": "2.2.5",
|
||||
"node-worker-threads-pool": "^1.4.3",
|
||||
"ws": "^7.4.4"
|
||||
"ws": "^7.4.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/compression": "^1.0.1",
|
||||
"@types/express": "^4.17.2",
|
||||
"@types/locutus": "^0.0.6",
|
||||
"@types/ws": "^6.0.4",
|
||||
"@types/ws": "^7.4.4",
|
||||
"tslint": "^6.1.0",
|
||||
"typescript": "^4.1.5"
|
||||
}
|
||||
@@ -163,10 +163,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.4.tgz",
|
||||
"integrity": "sha512-PpPrX7SZW9re6+Ha8ojZG4Se8AZXgf0GK6zmfqEuCsY49LFDNXO3SByp44X3dFEqtB73lkCDAdUazhAjVPiNwg==",
|
||||
"version": "7.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.4.tgz",
|
||||
"integrity": "sha512-d/7W23JAXPodQNbOZNXvl2K+bqAQrCMwlh/nuQsPSQk6Fq0opHoPrUw43aHsvSbIiQPr8Of2hkFbnz1XBFVyZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@@ -548,17 +549,17 @@
|
||||
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
|
||||
},
|
||||
"node_modules/elliptic": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
|
||||
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
|
||||
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
|
||||
"dependencies": {
|
||||
"bn.js": "^4.4.0",
|
||||
"brorand": "^1.0.1",
|
||||
"bn.js": "^4.11.9",
|
||||
"brorand": "^1.1.0",
|
||||
"hash.js": "^1.0.0",
|
||||
"hmac-drbg": "^1.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
"minimalistic-crypto-utils": "^1.0.0"
|
||||
"hmac-drbg": "^1.0.1",
|
||||
"inherits": "^2.0.4",
|
||||
"minimalistic-assert": "^1.0.1",
|
||||
"minimalistic-crypto-utils": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
@@ -1544,9 +1545,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "7.4.4",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
|
||||
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
|
||||
"version": "7.4.6",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
||||
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
@@ -1698,9 +1699,9 @@
|
||||
}
|
||||
},
|
||||
"@types/ws": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.4.tgz",
|
||||
"integrity": "sha512-PpPrX7SZW9re6+Ha8ojZG4Se8AZXgf0GK6zmfqEuCsY49LFDNXO3SByp44X3dFEqtB73lkCDAdUazhAjVPiNwg==",
|
||||
"version": "7.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.4.tgz",
|
||||
"integrity": "sha512-d/7W23JAXPodQNbOZNXvl2K+bqAQrCMwlh/nuQsPSQk6Fq0opHoPrUw43aHsvSbIiQPr8Of2hkFbnz1XBFVyZQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
@@ -2042,17 +2043,17 @@
|
||||
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
|
||||
},
|
||||
"elliptic": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
|
||||
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
|
||||
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
|
||||
"requires": {
|
||||
"bn.js": "^4.4.0",
|
||||
"brorand": "^1.0.1",
|
||||
"bn.js": "^4.11.9",
|
||||
"brorand": "^1.1.0",
|
||||
"hash.js": "^1.0.0",
|
||||
"hmac-drbg": "^1.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
"minimalistic-crypto-utils": "^1.0.0"
|
||||
"hmac-drbg": "^1.0.1",
|
||||
"inherits": "^2.0.4",
|
||||
"minimalistic-assert": "^1.0.1",
|
||||
"minimalistic-crypto-utils": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"encodeurl": {
|
||||
@@ -2833,9 +2834,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.4.4",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
|
||||
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
|
||||
"version": "7.4.6",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
||||
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
|
||||
"requires": {}
|
||||
},
|
||||
"yallist": {
|
||||
|
||||
@@ -37,13 +37,13 @@
|
||||
"locutus": "^2.0.12",
|
||||
"mysql2": "2.2.5",
|
||||
"node-worker-threads-pool": "^1.4.3",
|
||||
"ws": "^7.4.4"
|
||||
"ws": "^7.4.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/compression": "^1.0.1",
|
||||
"@types/express": "^4.17.2",
|
||||
"@types/locutus": "^0.0.6",
|
||||
"@types/ws": "^6.0.4",
|
||||
"@types/ws": "^7.4.4",
|
||||
"tslint": "^6.1.0",
|
||||
"typescript": "^4.1.5"
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@ import { StaticPool } from 'node-worker-threads-pool';
|
||||
import logger from '../../logger';
|
||||
|
||||
class Bisq {
|
||||
private static BLOCKS_JSON_FILE_PATH = config.BISQ_BLOCKS.DATA_PATH + '/all/blocks.json';
|
||||
private static BLOCKS_JSON_FILE_PATH = config.BISQ.DATA_PATH + '/json/all/blocks.json';
|
||||
private latestBlockHeight = 0;
|
||||
private blocks: BisqBlock[] = [];
|
||||
private allBlocks: BisqBlock[] = [];
|
||||
private transactions: BisqTransaction[] = [];
|
||||
private transactionIndex: { [txId: string]: BisqTransaction } = {};
|
||||
private blockIndex: { [hash: string]: BisqBlock } = {};
|
||||
@@ -98,7 +99,7 @@ class Bisq {
|
||||
this.topDirectoryWatcher.close();
|
||||
}
|
||||
let fsWait: NodeJS.Timeout | null = null;
|
||||
this.topDirectoryWatcher = fs.watch(config.BISQ_BLOCKS.DATA_PATH, () => {
|
||||
this.topDirectoryWatcher = fs.watch(config.BISQ.DATA_PATH + '/json', () => {
|
||||
if (fsWait) {
|
||||
clearTimeout(fsWait);
|
||||
}
|
||||
@@ -126,7 +127,7 @@ class Bisq {
|
||||
return;
|
||||
}
|
||||
let fsWait: NodeJS.Timeout | null = null;
|
||||
this.subdirectoryWatcher = fs.watch(config.BISQ_BLOCKS.DATA_PATH + '/all', () => {
|
||||
this.subdirectoryWatcher = fs.watch(config.BISQ.DATA_PATH + '/json/all', () => {
|
||||
if (fsWait) {
|
||||
clearTimeout(fsWait);
|
||||
}
|
||||
@@ -171,7 +172,7 @@ class Bisq {
|
||||
this.transactionIndex = {};
|
||||
this.addressIndex = {};
|
||||
|
||||
this.blocks.forEach((block) => {
|
||||
this.allBlocks.forEach((block) => {
|
||||
/* Build block index */
|
||||
if (!this.blockIndex[block.hash]) {
|
||||
this.blockIndex[block.hash] = block;
|
||||
@@ -245,9 +246,10 @@ class Bisq {
|
||||
if (cacheData && cacheData.length !== 0) {
|
||||
logger.debug('Processing Bisq data dump...');
|
||||
const data: BisqBlocks = await this.jsonParsePool.exec(cacheData);
|
||||
if (data.blocks && data.blocks.length !== this.blocks.length) {
|
||||
this.blocks = data.blocks.filter((block) => block.txs.length > 0);
|
||||
this.blocks.reverse();
|
||||
if (data.blocks && data.blocks.length !== this.allBlocks.length) {
|
||||
this.allBlocks = data.blocks;
|
||||
this.allBlocks.reverse();
|
||||
this.blocks = this.allBlocks.filter((block) => block.txs.length > 0);
|
||||
this.latestBlockHeight = data.chainHeight;
|
||||
const time = new Date().getTime() - start;
|
||||
logger.debug('Bisq dump processed in ' + time + ' ms (worker thread)');
|
||||
|
||||
@@ -457,6 +457,30 @@ class BisqMarketsApi {
|
||||
}
|
||||
}
|
||||
|
||||
getVolumesByTime(time: number): MarketVolume[] {
|
||||
const timestamp_from = new Date().getTime() / 1000 - time;
|
||||
const timestamp_to = new Date().getTime() / 1000;
|
||||
|
||||
const trades = this.getTradesByCriteria(undefined, timestamp_to, timestamp_from,
|
||||
undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER);
|
||||
|
||||
const markets: any = {};
|
||||
|
||||
for (const trade of trades) {
|
||||
if (!markets[trade._market]) {
|
||||
markets[trade._market] = {
|
||||
'volume': 0,
|
||||
'num_trades': 0,
|
||||
};
|
||||
}
|
||||
|
||||
markets[trade._market]['volume'] += this.fiatCurrenciesIndexed[trade.currency] ? trade._tradeAmount : trade._tradeVolume;
|
||||
markets[trade._market]['num_trades']++;
|
||||
}
|
||||
|
||||
return markets;
|
||||
}
|
||||
|
||||
private getTradesSummarized(trades: TradesData[], timestamp_from: number, interval?: string): SummarizedIntervals {
|
||||
const intervals: any = {};
|
||||
const intervals_prices: any = {};
|
||||
|
||||
@@ -6,7 +6,7 @@ import logger from '../../logger';
|
||||
|
||||
class Bisq {
|
||||
private static FOLDER_WATCH_CHANGE_DETECTION_DEBOUNCE = 4000;
|
||||
private static MARKET_JSON_PATH = config.BISQ_MARKETS.DATA_PATH;
|
||||
private static MARKET_JSON_PATH = config.BISQ.DATA_PATH;
|
||||
private static MARKET_JSON_FILE_PATHS = {
|
||||
activeCryptoCurrency: '/active_crypto_currency_list.json',
|
||||
activeFiatCurrency: '/active_fiat_currency_list.json',
|
||||
|
||||
@@ -147,6 +147,9 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, addPrevout);
|
||||
} else {
|
||||
esploraTransaction = await this.$appendMempoolFeeData(esploraTransaction);
|
||||
if (addPrevout) {
|
||||
esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, addPrevout);
|
||||
}
|
||||
}
|
||||
|
||||
return esploraTransaction;
|
||||
|
||||
@@ -43,7 +43,7 @@ class FeeApi {
|
||||
const multiplier = (pBlock.blockVSize - 500000) / 500000;
|
||||
return Math.max(Math.round(useFee * multiplier), this.defaultFee);
|
||||
}
|
||||
return Math.round(useFee);
|
||||
return Math.ceil(useFee);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class FiatConversion {
|
||||
|
||||
public startService() {
|
||||
logger.info('Starting currency rates service');
|
||||
setInterval(this.updateCurrency.bind(this), 1000 * 60 * 60);
|
||||
setInterval(this.updateCurrency.bind(this), 1000 * 60);
|
||||
this.updateCurrency();
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class WebsocketHandler {
|
||||
|
||||
this.wss.on('connection', (client: WebSocket) => {
|
||||
client.on('error', logger.info);
|
||||
client.on('message', (message: string) => {
|
||||
client.on('message', async (message: string) => {
|
||||
try {
|
||||
const parsedMessage: WebsocketResponse = JSON.parse(message);
|
||||
const response = {};
|
||||
@@ -53,7 +53,16 @@ class WebsocketHandler {
|
||||
if (parsedMessage['watch-mempool']) {
|
||||
const tx = memPool.getMempool()[client['track-tx']];
|
||||
if (tx) {
|
||||
response['tx'] = tx;
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
try {
|
||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
||||
response['tx'] = fullTx;
|
||||
} catch (e) {
|
||||
logger.debug('Error finding transaction in mempool: ' + e.message || e);
|
||||
}
|
||||
} else {
|
||||
response['tx'] = tx;
|
||||
}
|
||||
} else {
|
||||
client['track-mempool-tx'] = parsedMessage['track-tx'];
|
||||
}
|
||||
@@ -96,6 +105,14 @@ class WebsocketHandler {
|
||||
client['track-donation'] = parsedMessage['track-donation'];
|
||||
}
|
||||
|
||||
if (parsedMessage['track-bisq-market']) {
|
||||
if (/^[a-z]{3}_[a-z]{3}$/.test(parsedMessage['track-bisq-market'])) {
|
||||
client['track-bisq-market'] = parsedMessage['track-bisq-market'];
|
||||
} else {
|
||||
client['track-bisq-market'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(response).length) {
|
||||
client.send(JSON.stringify(response));
|
||||
}
|
||||
|
||||
@@ -52,11 +52,7 @@ interface IConfig {
|
||||
ENABLED: boolean;
|
||||
TX_PER_SECOND_SAMPLE_PERIOD: number;
|
||||
};
|
||||
BISQ_BLOCKS: {
|
||||
ENABLED: boolean;
|
||||
DATA_PATH: string;
|
||||
};
|
||||
BISQ_MARKETS: {
|
||||
BISQ: {
|
||||
ENABLED: boolean;
|
||||
DATA_PATH: string;
|
||||
};
|
||||
@@ -114,11 +110,7 @@ const defaults: IConfig = {
|
||||
'ENABLED': true,
|
||||
'TX_PER_SECOND_SAMPLE_PERIOD': 150
|
||||
},
|
||||
'BISQ_BLOCKS': {
|
||||
'ENABLED': false,
|
||||
'DATA_PATH': '/bisq/statsnode-data/btc_mainnet/db/json'
|
||||
},
|
||||
'BISQ_MARKETS': {
|
||||
'BISQ': {
|
||||
'ENABLED': false,
|
||||
'DATA_PATH': '/bisq/statsnode-data/btc_mainnet/db'
|
||||
},
|
||||
@@ -133,8 +125,7 @@ class Config implements IConfig {
|
||||
DATABASE: IConfig['DATABASE'];
|
||||
SYSLOG: IConfig['SYSLOG'];
|
||||
STATISTICS: IConfig['STATISTICS'];
|
||||
BISQ_BLOCKS: IConfig['BISQ_BLOCKS'];
|
||||
BISQ_MARKETS: IConfig['BISQ_MARKETS'];
|
||||
BISQ: IConfig['BISQ'];
|
||||
|
||||
constructor() {
|
||||
const configs = this.merge(configFile, defaults);
|
||||
@@ -146,8 +137,7 @@ class Config implements IConfig {
|
||||
this.DATABASE = configs.DATABASE;
|
||||
this.SYSLOG = configs.SYSLOG;
|
||||
this.STATISTICS = configs.STATISTICS;
|
||||
this.BISQ_BLOCKS = configs.BISQ_BLOCKS;
|
||||
this.BISQ_MARKETS = configs.BISQ_MARKETS;
|
||||
this.BISQ = configs.BISQ;
|
||||
}
|
||||
|
||||
merge = (...objects: object[]): IConfig => {
|
||||
|
||||
@@ -81,7 +81,7 @@ class Server {
|
||||
await checkDbConnection();
|
||||
}
|
||||
|
||||
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) {
|
||||
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isMaster) {
|
||||
statistics.startStatistics();
|
||||
}
|
||||
|
||||
@@ -90,13 +90,10 @@ class Server {
|
||||
this.setUpHttpApiRoutes();
|
||||
this.runMainUpdateLoop();
|
||||
|
||||
if (config.BISQ_BLOCKS.ENABLED) {
|
||||
if (config.BISQ.ENABLED) {
|
||||
bisq.startBisqService();
|
||||
bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price));
|
||||
blocks.setNewBlockCallback(bisq.handleNewBitcoinBlock.bind(bisq));
|
||||
}
|
||||
|
||||
if (config.BISQ_MARKETS.ENABLED) {
|
||||
bisqMarkets.startBisqService();
|
||||
}
|
||||
|
||||
@@ -210,7 +207,7 @@ class Server {
|
||||
;
|
||||
}
|
||||
|
||||
if (config.BISQ_BLOCKS.ENABLED) {
|
||||
if (config.BISQ.ENABLED) {
|
||||
this.app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/stats', routes.getBisqStats)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/tx/:txId', routes.getBisqTransaction)
|
||||
@@ -219,11 +216,6 @@ class Server {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/blocks/:index/:length', routes.getBisqBlocks)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/address/:address', routes.getBisqAddress)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/txs/:index/:length', routes.getBisqTransactions)
|
||||
;
|
||||
}
|
||||
|
||||
if (config.BISQ_MARKETS.ENABLED) {
|
||||
this.app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/currencies', routes.getBisqMarketCurrencies.bind(routes))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/depth', routes.getBisqMarketDepth.bind(routes))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/hloc', routes.getBisqMarketHloc.bind(routes))
|
||||
@@ -232,6 +224,7 @@ class Server {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/ticker', routes.getBisqMarketTicker.bind(routes))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/trades', routes.getBisqMarketTrades.bind(routes))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/volumes', routes.getBisqMarketVolumes.bind(routes))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/volumes/7d', routes.getBisqMarketVolumes7d.bind(routes))
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ class Logger {
|
||||
}
|
||||
|
||||
private getNetwork(): string {
|
||||
if (config.BISQ_BLOCKS.ENABLED) {
|
||||
if (config.BISQ.ENABLED) {
|
||||
return 'bisq';
|
||||
}
|
||||
if (config.MEMPOOL.NETWORK && config.MEMPOOL.NETWORK !== 'mainnet') {
|
||||
|
||||
@@ -144,6 +144,7 @@ export interface WebsocketResponse {
|
||||
'track-tx': string;
|
||||
'track-address': string;
|
||||
'watch-mempool': boolean;
|
||||
'track-bisq-market': string;
|
||||
}
|
||||
|
||||
export interface VbytesPerSecond {
|
||||
|
||||
@@ -426,6 +426,15 @@ class Routes {
|
||||
}
|
||||
}
|
||||
|
||||
public getBisqMarketVolumes7d(req: Request, res: Response) {
|
||||
const result = bisqMarket.getVolumesByTime(604800);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketVolumes7d error'));
|
||||
}
|
||||
}
|
||||
|
||||
private parseRequestParameters(requestParams: object, params: RequiredSpec): { [name: string]: any; } {
|
||||
const final = {};
|
||||
for (const i in params) {
|
||||
|
||||
5
frontend/.gitignore
vendored
5
frontend/.gitignore
vendored
@@ -54,3 +54,8 @@ src/resources/pools.json
|
||||
# environment config
|
||||
mempool-frontend-config.json
|
||||
generated-config.js
|
||||
|
||||
# e2e results
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ https://www.transifex.com/mempool/mempool/dashboard/
|
||||
* Dutch @m__btc
|
||||
* Japanese @wiz @japananon
|
||||
* Norwegian @T82771355
|
||||
* Polish @maciejsoltysiak
|
||||
* Portugese @jgcastro1985
|
||||
* Slovenian @thepkbadger
|
||||
* Finnish @bio_bitcoin
|
||||
@@ -30,3 +31,4 @@ https://www.transifex.com/mempool/mempool/dashboard/
|
||||
* Ukrainian @volbil
|
||||
* Vietnamese @bitcoin_vietnam
|
||||
* Chinese @wdljt
|
||||
* Russian @TonyCrusoe @Bitconan
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
"prefix": "app",
|
||||
"i18n": {
|
||||
"sourceLocale": {
|
||||
"code":"en-US",
|
||||
"baseHref":"/"
|
||||
"code": "en-US",
|
||||
"baseHref": "/"
|
||||
},
|
||||
"locales": {
|
||||
"ar": {
|
||||
@@ -71,6 +71,10 @@
|
||||
"translation": "src/locale/messages.nb.xlf",
|
||||
"baseHref": "/nb/"
|
||||
},
|
||||
"pl": {
|
||||
"translation": "src/locale/messages.pl.xlf",
|
||||
"baseHref": "/pl/"
|
||||
},
|
||||
"pt": {
|
||||
"translation": "src/locale/messages.pt.xlf",
|
||||
"baseHref": "/pt/"
|
||||
@@ -106,6 +110,10 @@
|
||||
"zh": {
|
||||
"translation": "src/locale/messages.zh.xlf",
|
||||
"baseHref": "/zh/"
|
||||
},
|
||||
"ru": {
|
||||
"translation": "src/locale/messages.ru.xlf",
|
||||
"baseHref": "/ru/"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -169,6 +177,22 @@
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "mempool:build:production"
|
||||
},
|
||||
"local": {
|
||||
"proxyConfig": "proxy.conf.json",
|
||||
"verbose": true
|
||||
},
|
||||
"staging": {
|
||||
"proxyConfig": "proxy.stg.conf.json",
|
||||
"disableHostCheck": true,
|
||||
"host": "0.0.0.0",
|
||||
"verbose": true
|
||||
},
|
||||
"local-prod": {
|
||||
"proxyConfig": "proxy.prod.conf.json",
|
||||
"disableHostCheck": true,
|
||||
"host": "0.0.0.0",
|
||||
"verbose": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -201,8 +225,8 @@
|
||||
"tsConfig": [
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.spec.json",
|
||||
"e2e/tsconfig.json",
|
||||
"tsconfig.server.json"
|
||||
"tsconfig.server.json",
|
||||
"cypress/tsconfig.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
@@ -210,10 +234,11 @@
|
||||
}
|
||||
},
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"builder": "@cypress/schematic:cypress",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js",
|
||||
"devServerTarget": "mempool:serve"
|
||||
"devServerTarget": "mempool:serve:local-prod",
|
||||
"watch": true,
|
||||
"headless": false
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
@@ -268,8 +293,27 @@
|
||||
"configurations": {
|
||||
"production": {}
|
||||
}
|
||||
},
|
||||
"cypress-run": {
|
||||
"builder": "@cypress/schematic:cypress",
|
||||
"options": {
|
||||
"devServerTarget": "mempool:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "mempool:serve:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cypress-open": {
|
||||
"builder": "@cypress/schematic:cypress",
|
||||
"options": {
|
||||
"watch": true,
|
||||
"headless": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}},
|
||||
}
|
||||
},
|
||||
"defaultProject": "mempool"
|
||||
}
|
||||
|
||||
9
frontend/cypress.json
Normal file
9
frontend/cypress.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"integrationFolder": "cypress/integration",
|
||||
"supportFile": "cypress/support/index.ts",
|
||||
"videosFolder": "cypress/videos",
|
||||
"screenshotsFolder": "cypress/screenshots",
|
||||
"pluginsFile": "cypress/plugins/index.js",
|
||||
"fixturesFolder": "cypress/fixtures",
|
||||
"baseUrl": "http://localhost:4200"
|
||||
}
|
||||
119
frontend/cypress/fixtures/assets.json
Normal file
119
frontend/cypress/fixtures/assets.json
Normal file
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"f59c5f3e8141f322276daa63ed5f307085808aea6d4ef9ba61e28154533fdec7": {
|
||||
"asset_id": "f59c5f3e8141f322276daa63ed5f307085808aea6d4ef9ba61e28154533fdec7",
|
||||
"contract": {
|
||||
"entity": {
|
||||
"domain": "listedreserve.com"
|
||||
},
|
||||
"issuer_pubkey": "031cc579d142a03b33cdd745922112821c16e5e8b74e3bd57f16f7fda872b6f1d0",
|
||||
"name": "Liquid AUD",
|
||||
"precision": 2,
|
||||
"ticker": "AUDL",
|
||||
"version": 0
|
||||
},
|
||||
"issuance_txin": {
|
||||
"txid": "e5c5144ba3dc48259ae29023fe9f7775dec1fc049f456dd3d1f7178e31901fb5",
|
||||
"vin": 0
|
||||
},
|
||||
"issuance_prevout": {
|
||||
"txid": "ed48be2e035ffa425d2c6faaa82b6a7b648aed1246b6ac76c72e0408db8cf057",
|
||||
"vout": 1
|
||||
},
|
||||
"name": "Liquid AUD",
|
||||
"ticker": "AUDL",
|
||||
"precision": 2,
|
||||
"entity": {
|
||||
"domain": "listedreserve.com"
|
||||
},
|
||||
"version": 0,
|
||||
"issuer_pubkey": "031cc579d142a03b33cdd745922112821c16e5e8b74e3bd57f16f7fda872b6f1d0"
|
||||
},
|
||||
"0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a": {
|
||||
"asset_id": "0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a",
|
||||
"contract": {
|
||||
"entity": {
|
||||
"domain": "lcad.bullbitcoin.com"
|
||||
},
|
||||
"issuer_pubkey": "027fa34026195b05f3aa217335416811dca4f5b579d00271a1bb6304c0152458a8",
|
||||
"name": "Liquid CAD",
|
||||
"precision": 8,
|
||||
"ticker": "LCAD",
|
||||
"version": 0
|
||||
},
|
||||
"issuance_txin": {
|
||||
"txid": "238badf029cadcf546d90ce23c7eafc2fa2082585c9bd62dc26f1aa11c7bd850",
|
||||
"vin": 0
|
||||
},
|
||||
"issuance_prevout": {
|
||||
"txid": "a87f13917c08c7ccd8eddb1830c5c9a2bcd59c7d167e9d528659ba40808a6b76",
|
||||
"vout": 0
|
||||
},
|
||||
"name": "Liquid CAD",
|
||||
"ticker": "LCAD",
|
||||
"precision": 8,
|
||||
"entity": {
|
||||
"domain": "lcad.bullbitcoin.com"
|
||||
},
|
||||
"version": 0,
|
||||
"issuer_pubkey": "027fa34026195b05f3aa217335416811dca4f5b579d00271a1bb6304c0152458a8"
|
||||
},
|
||||
"3438ecb49fc45c08e687de4749ed628c511e326460ea4336794e1cf02741329e": {
|
||||
"asset_id": "3438ecb49fc45c08e687de4749ed628c511e326460ea4336794e1cf02741329e",
|
||||
"contract": {
|
||||
"entity": {
|
||||
"domain": "settlenet.io"
|
||||
},
|
||||
"issuer_pubkey": "037b09d542bf7cea6a19fa624b4441790c1a6e44823597bf190e981a846a196541",
|
||||
"name": "SETTLENET JPY Stablecoin by Crypto Garage",
|
||||
"precision": 0,
|
||||
"ticker": "JPYS",
|
||||
"version": 0
|
||||
},
|
||||
"issuance_txin": {
|
||||
"txid": "e33ad5ce8879297d8bfa7daa193920b94abd3fb12f4e8dade9543dbb292387cb",
|
||||
"vin": 0
|
||||
},
|
||||
"issuance_prevout": {
|
||||
"txid": "328c4fadd817ea75e634e3648eb4be0bf7e669539b8da921c0f77af3bc148894",
|
||||
"vout": 1
|
||||
},
|
||||
"name": "SETTLENET JPY Stablecoin by Crypto Garage",
|
||||
"ticker": "JPYS",
|
||||
"precision": 0,
|
||||
"entity": {
|
||||
"domain": "settlenet.io"
|
||||
},
|
||||
"version": 0,
|
||||
"issuer_pubkey": "037b09d542bf7cea6a19fa624b4441790c1a6e44823597bf190e981a846a196541"
|
||||
},
|
||||
"ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2": {
|
||||
"asset_id": "ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2",
|
||||
"contract": {
|
||||
"entity": {
|
||||
"domain": "tether.to"
|
||||
},
|
||||
"issuer_pubkey": "0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904",
|
||||
"name": "Tether USD",
|
||||
"precision": 8,
|
||||
"ticker": "USDt",
|
||||
"version": 0
|
||||
},
|
||||
"issuance_txin": {
|
||||
"txid": "abb4080d91849e933ee2ed65da6b436f7c385cf363fb4aa08399f1e27c58ff3d",
|
||||
"vin": 0
|
||||
},
|
||||
"issuance_prevout": {
|
||||
"txid": "9596d259270ef5bac0020435e6d859aea633409483ba64e232b8ba04ce288668",
|
||||
"vout": 0
|
||||
},
|
||||
"name": "Tether USD",
|
||||
"ticker": "USDt",
|
||||
"precision": 8,
|
||||
"entity": {
|
||||
"domain": "tether.to"
|
||||
},
|
||||
"version": 0,
|
||||
"issuer_pubkey": "0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904"
|
||||
}
|
||||
}
|
||||
|
||||
33
frontend/cypress/fixtures/assets.minimal.json
Normal file
33
frontend/cypress/fixtures/assets.minimal.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"f59c5f3e8141f322276daa63ed5f307085808aea6d4ef9ba61e28154533fdec7": [
|
||||
"listedreserve.com",
|
||||
"AUDL",
|
||||
"Liquid AUD",
|
||||
2
|
||||
],
|
||||
"0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a": [
|
||||
"lcad.bullbitcoin.com",
|
||||
"LCAD",
|
||||
"Liquid CAD",
|
||||
8
|
||||
],
|
||||
"6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d": [
|
||||
null,
|
||||
"L-BTC",
|
||||
"Liquid Bitcoin",
|
||||
8
|
||||
],
|
||||
"ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2": [
|
||||
"tether.to",
|
||||
"USDt",
|
||||
"Tether USD",
|
||||
8
|
||||
],
|
||||
"3438ecb49fc45c08e687de4749ed628c511e326460ea4336794e1cf02741329e": [
|
||||
"settlenet.io",
|
||||
"JPYS",
|
||||
"SETTLENET JPY Stablecoin by Crypto Garage",
|
||||
0
|
||||
]
|
||||
}
|
||||
|
||||
1178
frontend/cypress/fixtures/pools.json
Normal file
1178
frontend/cypress/fixtures/pools.json
Normal file
File diff suppressed because it is too large
Load Diff
51
frontend/cypress/integration/bisq/bisq.spec.ts
Normal file
51
frontend/cypress/integration/bisq/bisq.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
describe('Bisq', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
cy.intercept('/sockjs-node/info*').as('socket');
|
||||
cy.intercept('/bisq/api/markets/hloc?market=btc_usd&interval=day').as('hloc');
|
||||
cy.intercept('/bisq/api/markets/ticker').as('ticker');
|
||||
cy.intercept('/bisq/api/markets/markets').as('markets');
|
||||
cy.intercept('/bisq/api/markets/volumes/7d').as('7d');
|
||||
cy.intercept('/bisq/api/markets/trades?market=all').as('trades');
|
||||
cy.intercept('/bisq/api/txs/*/*').as('txs');
|
||||
cy.intercept('/bisq/api/blocks/*/*').as('blocks');
|
||||
cy.intercept('/bisq/api/stats').as('stats');
|
||||
});
|
||||
it('loads the dashboard', () => {
|
||||
cy.visit('/bisq');
|
||||
|
||||
cy.wait('@socket');
|
||||
cy.wait('@hloc');
|
||||
cy.wait('@ticker');
|
||||
cy.wait('@markets');
|
||||
cy.wait('@7d');
|
||||
cy.wait('@trades');
|
||||
});
|
||||
|
||||
it('loads the transactions screen', () => {
|
||||
cy.visit('/bisq');
|
||||
cy.get('li:nth-of-type(2) > a').click().then(() => {
|
||||
cy.wait('@txs');
|
||||
});
|
||||
});
|
||||
it('loads the blocks screen', () => {
|
||||
cy.visit('/bisq');
|
||||
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
||||
cy.wait('@blocks');
|
||||
});
|
||||
});
|
||||
it('loads the stats screen', () => {
|
||||
cy.visit('/bisq');
|
||||
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
||||
cy.wait('@stats');
|
||||
});
|
||||
});
|
||||
|
||||
it('loads the api screen', () => {
|
||||
cy.visit('/bisq');
|
||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
81
frontend/cypress/integration/liquid/liquid.spec.ts
Normal file
81
frontend/cypress/integration/liquid/liquid.spec.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
describe('Liquid', () => {
|
||||
beforeEach(() => {
|
||||
// TODO: Fix ng serve to deliver these files
|
||||
cy.fixture('assets.minimal').then((json) => {
|
||||
cy.intercept('/resources/assets.minimal.json', json);
|
||||
});
|
||||
|
||||
cy.fixture('assets').then((json) => {
|
||||
cy.intercept('/resources/assets.json', json);
|
||||
});
|
||||
});
|
||||
|
||||
it('loads the dashboard', () => {
|
||||
cy.visit('/liquid');
|
||||
});
|
||||
|
||||
it('loads the blocks page', () => {
|
||||
cy.visit('/liquid/blocks');
|
||||
});
|
||||
|
||||
it('loads a specific block page', () => {
|
||||
cy.visit('/liquid/blocks');
|
||||
});
|
||||
|
||||
it('loads the graphs page', () => {
|
||||
cy.visit('/liquid/graphs');
|
||||
});
|
||||
|
||||
it('loads the tv page - desktop', () => {
|
||||
cy.visit('/liquid');
|
||||
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
||||
cy.wait(1000);
|
||||
});
|
||||
});
|
||||
|
||||
it('loads the graphs page - mobile', () => {
|
||||
cy.visit('/liquid');
|
||||
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
||||
cy.viewport('iphone-6');
|
||||
cy.wait(1000);
|
||||
// TODO: Should we really support TV Mode in Mobile for Bisq?
|
||||
// cy.get('.tv-only').should('be.visible')
|
||||
});
|
||||
});
|
||||
|
||||
describe('assets', () => {
|
||||
it('shows the assets screen', () => {
|
||||
cy.visit('/liquid');
|
||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
||||
cy.get('table tr').should('have.length', 5);
|
||||
});
|
||||
});
|
||||
|
||||
it('allows searching assets', () => {
|
||||
cy.visit('/liquid');
|
||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
||||
cy.get('.container-xl input').click().type('Liquid Bitcoin').then(() => {
|
||||
cy.get('table tr').should('have.length', 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a specific asset ID', () => {
|
||||
cy.visit('/liquid');
|
||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
||||
cy.get('.container-xl input').click().type('Liquid CAD').then(() => {
|
||||
cy.get('table tr td:nth-of-type(4) a').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a specific asset issuance TX', () => {
|
||||
cy.visit('/liquid');
|
||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
||||
cy.get('.container-xl input').click().type('Liquid CAD').then(() => {
|
||||
cy.get('table tr td:nth-of-type(5) a').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
97
frontend/cypress/integration/mainnet/mainnet.spec.ts
Normal file
97
frontend/cypress/integration/mainnet/mainnet.spec.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
describe('Mainnet', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('/api/block-height/*').as('block-height');
|
||||
cy.intercept('/api/block/*').as('block');
|
||||
cy.intercept('/api/block/*/txs/0').as('block-txs');
|
||||
cy.intercept('/api/tx/*/outspends').as('tx-outspends');
|
||||
|
||||
// TODO: Fix ng serve to deliver this file
|
||||
cy.fixture('pools').then((json) => {
|
||||
cy.intercept('/resources/pools.json', json);
|
||||
});
|
||||
});
|
||||
|
||||
it('loads the dashboard', () => {
|
||||
cy.visit('/');
|
||||
cy.wait(1000);
|
||||
});
|
||||
|
||||
it('loads the blocks screen', () => {
|
||||
cy.visit('/');
|
||||
cy.get('li:nth-of-type(2) > a').click().then(() => {
|
||||
cy.wait(1000);
|
||||
});
|
||||
});
|
||||
|
||||
it('loads the graphs screen', () => {
|
||||
cy.visit('/');
|
||||
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
||||
cy.wait(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tv mode', () => {
|
||||
it('loads the tv screen - desktop', () => {
|
||||
cy.viewport('macbook-16');
|
||||
cy.visit('/');
|
||||
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
||||
cy.wait(1000);
|
||||
cy.get('.tv-only').should('not.be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
it('loads the tv screen - mobile', () => {
|
||||
cy.visit('/');
|
||||
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
||||
cy.viewport('iphone-6');
|
||||
cy.wait(1000);
|
||||
cy.get('.tv-only').should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('loads the api screen', () => {
|
||||
cy.visit('/');
|
||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
||||
cy.wait(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('blocks', () => {
|
||||
it('shows empty blocks properly', () => {
|
||||
cy.visit('/block/0000000000000000000bd14f744ef2e006e61c32214670de7eb891a5732ee775');
|
||||
cy.get('h2').invoke('text').should('equal', '1 transaction');
|
||||
});
|
||||
|
||||
it('expands and collapses the block details', () => {
|
||||
cy.visit('/block/0');
|
||||
cy.wait('@tx-outspends');
|
||||
cy.get('.btn.btn-outline-info').click().then(() => {
|
||||
cy.get('#details').should('be.visible');
|
||||
});
|
||||
|
||||
cy.get('.btn.btn-outline-info').click().then(() => {
|
||||
cy.get('#details').should('not.be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
it('shows blocks with no pagination', () => {
|
||||
cy.visit('/block/00000000000000000001ba40caf1ad4cec0ceb77692662315c151953bfd7c4c4');
|
||||
cy.get('h2').invoke('text').should('equal', '19 transactions');
|
||||
cy.get('ul.pagination').first().children().should('have.length', 5);
|
||||
});
|
||||
|
||||
it('supports pagination on the block screen', () => {
|
||||
// 41 txs
|
||||
cy.visit('/block/00000000000000000009f9b7b0f63ad50053ad12ec3b7f5ca951332f134f83d8');
|
||||
cy.get('.header-bg.box > a').invoke('text').then((text1) => {
|
||||
cy.get('.active + li').first().click().then(() => {
|
||||
cy.get('.header-bg.box > a').invoke('text').then((text2) => {
|
||||
expect(text1).not.to.eq(text2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
10
frontend/cypress/integration/signet/signet.spec.ts
Normal file
10
frontend/cypress/integration/signet/signet.spec.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
describe('Signet', () => {
|
||||
it('loads the dashboard', () => {
|
||||
cy.visit('/signet');
|
||||
});
|
||||
|
||||
it.skip('loads all the pages properly', () => {
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
9
frontend/cypress/integration/testnet/testnet.spec.ts
Normal file
9
frontend/cypress/integration/testnet/testnet.spec.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
describe('Testnet', () => {
|
||||
it('loads the dashboard', () => {
|
||||
cy.visit('/testnet');
|
||||
});
|
||||
|
||||
it.skip('loads all the pages properly', () => {
|
||||
|
||||
});
|
||||
});
|
||||
1
frontend/cypress/plugins/index.js
Normal file
1
frontend/cypress/plugins/index.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = (on, config) => {}
|
||||
45
frontend/cypress/support/commands.ts
Normal file
45
frontend/cypress/support/commands.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// ***********************************************
|
||||
// This example namespace declaration will help
|
||||
// with Intellisense and code completion in your
|
||||
// IDE or Text Editor.
|
||||
// ***********************************************
|
||||
// declare namespace Cypress {
|
||||
// interface Chainable<Subject = any> {
|
||||
// customCommand(param: any): typeof customCommand;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// function customCommand(param: any): void {
|
||||
// console.warn(param);
|
||||
// }
|
||||
//
|
||||
// NOTE: You can use it like so:
|
||||
// Cypress.Commands.add('customCommand', customCommand);
|
||||
//
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add("login", (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
|
||||
import 'cypress-wait-until';
|
||||
17
frontend/cypress/support/index.ts
Normal file
17
frontend/cypress/support/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// When a command from ./commands is ready to use, import with `import './commands'` syntax
|
||||
import './commands';
|
||||
8
frontend/cypress/tsconfig.json
Normal file
8
frontend/cypress/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"sourceMap": false,
|
||||
"types": ["cypress"]
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// @ts-check
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||
|
||||
/**
|
||||
* @type { import("protractor").Config }
|
||||
*/
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./src/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
'browserName': 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: require('path').join(__dirname, './tsconfig.json')
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||
}
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import { AppPage } from './app.po';
|
||||
import { browser, logging } from 'protractor';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', () => {
|
||||
page.navigateTo();
|
||||
expect(page.getTitleText()).toEqual('Welcome to mempool!');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Assert that there are no errors emitted from the browser
|
||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||
expect(logs).not.toContain(jasmine.objectContaining({
|
||||
level: logging.Level.SEVERE,
|
||||
} as logging.Entry));
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
navigateTo() {
|
||||
return browser.get(browser.baseUrl) as Promise<any>;
|
||||
}
|
||||
|
||||
getTitleText() {
|
||||
return element(by.css('app-root h1')).getText() as Promise<string>;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"module": "commonjs",
|
||||
"target": "es2018",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"jasminewd2",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
var fs = require('fs');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
|
||||
const GENERATED_CONFIG_FILE_NAME = 'generated-config.js';
|
||||
@@ -32,12 +33,13 @@ for (setting in configContent) {
|
||||
}
|
||||
|
||||
try {
|
||||
gitCommitHash = fs.readFileSync('../.git/refs/heads/master').toString().trim();
|
||||
const command = 'git rev-parse --short HEAD';
|
||||
gitCommitHash = execSync(command).toString('utf8').replace(/[\n\r\s]+$/, '');
|
||||
} catch (e) {
|
||||
console.log('Could not load git commit info: ' + e.message || e);
|
||||
}
|
||||
|
||||
const code = `(function (window) {
|
||||
const newConfig = `(function (window) {
|
||||
window.__env = window.__env || {};${settings.reduce((str, obj) => `${str}
|
||||
window.__env.${obj.key} = ${ typeof obj.value === 'string' ? `'${obj.value}'` : obj.value };`, '')}
|
||||
window.__env.GIT_COMMIT_HASH = '${gitCommitHash}';
|
||||
@@ -45,9 +47,17 @@ const code = `(function (window) {
|
||||
}(global || this));`;
|
||||
|
||||
try {
|
||||
fs.writeFileSync(GENERATED_CONFIG_FILE_NAME, code, 'utf8');
|
||||
const currentConfig = fs.readFileSync(GENERATED_CONFIG_FILE_NAME).toString().trim();
|
||||
if (currentConfig === newConfig) {
|
||||
console.log("Configuration not changed, skipping generation");
|
||||
} else {
|
||||
try {
|
||||
fs.writeFileSync(GENERATED_CONFIG_FILE_NAME, newConfig, 'utf8');
|
||||
console.log('Config file generated');
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
|
||||
console.log('Config file generated');
|
||||
24177
frontend/package-lock.json
generated
24177
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -24,8 +24,12 @@
|
||||
"tsc": "./node_modules/typescript/bin/tsc",
|
||||
"i18n-extract-from-source": "./node_modules/@angular/cli/bin/ng xi18n --ivy --out-file ./src/locale/messages.xlf",
|
||||
"i18n-pull-from-transifex": "tx pull -a --parallel --minimum-perc 1 --force",
|
||||
"serve": "ng serve --proxy-config proxy.conf.json",
|
||||
"start": "npm run generate-config && npm run sync-assets-dev && ng serve --proxy-config proxy.conf.json",
|
||||
"serve": "npm run generate-config && ng serve -c local",
|
||||
"serve:stg": "npm run generate-config && ng serve -c staging",
|
||||
"serve:local-prod": "npm run generate-config && ng serve -c local-prod",
|
||||
"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:local-prod": "npm run generate-config && npm run sync-assets-dev && ng serve -c local-prod",
|
||||
"build": "npm run generate-config && ng build --prod --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-dev": "node sync-assets.js dev",
|
||||
@@ -37,7 +41,9 @@
|
||||
"dev:ssr": "npm run generate-config && ng run mempool:serve-ssr",
|
||||
"serve:ssr": "node server.run.js",
|
||||
"build:ssr": "npm run build && ng run mempool:server:production && ./node_modules/typescript/bin/tsc server.run.ts",
|
||||
"prerender": "ng run mempool:prerender"
|
||||
"prerender": "ng run mempool:prerender",
|
||||
"cypress:open": "concurrently \"ng serve -c local-prod\" \"cypress open\" --kill-others",
|
||||
"cypress:run": "concurrently \"ng serve -c local-prod\" \"cypress run\" --kill-others"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "~11.2.8",
|
||||
@@ -55,7 +61,7 @@
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||
"@mempool/chartist": "^0.11.4",
|
||||
"@mempool/mempool.js": "^2.2.0",
|
||||
"@mempool/mempool.js": "^2.2.2",
|
||||
"@ng-bootstrap/ng-bootstrap": "^7.0.0",
|
||||
"@nguniversal/express-engine": "11.2.1",
|
||||
"@types/qrcode": "^1.3.4",
|
||||
@@ -64,6 +70,7 @@
|
||||
"clipboard": "^2.0.4",
|
||||
"domino": "^2.1.6",
|
||||
"express": "^4.17.1",
|
||||
"lightweight-charts": "^3.3.0",
|
||||
"ngx-bootrap-multiselect": "^2.0.0",
|
||||
"ngx-infinite-scroll": "^10.0.1",
|
||||
"qrcode": "^1.4.4",
|
||||
@@ -78,12 +85,15 @@
|
||||
"@angular/cli": "~11.2.7",
|
||||
"@angular/compiler-cli": "~11.2.8",
|
||||
"@angular/language-service": "~11.2.8",
|
||||
"@cypress/schematic": "^1.3.0",
|
||||
"@nguniversal/builders": "^11.2.1",
|
||||
"@types/express": "^4.17.0",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"@types/node": "^12.11.1",
|
||||
"codelyzer": "^6.0.1",
|
||||
"concurrently": "^6.2.0",
|
||||
"cypress-wait-until": "^1.7.1",
|
||||
"http-proxy-middleware": "^1.0.5",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
@@ -92,9 +102,11 @@
|
||||
"karma-coverage": "~2.0.3",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"protractor": "~7.0.0",
|
||||
"ts-node": "~8.3.0",
|
||||
"tslint": "~6.1.0",
|
||||
"typescript": "~4.1.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"cypress": "^7.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,5 +88,20 @@
|
||||
"pathRewrite": {
|
||||
"^/bisq/api": "/api/v1/ws"
|
||||
}
|
||||
},
|
||||
"/resources/assets.minimal.json": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
},
|
||||
"/resources/assets.json": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
},
|
||||
"/resources/pools.json": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
102
frontend/proxy.prod.conf.json
Normal file
102
frontend/proxy.prod.conf.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"/api/v1/ws": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"ws": true
|
||||
},
|
||||
"/api/*": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"logLevel": "debug",
|
||||
"pathRewrite": {
|
||||
"^/api": "https://mempool.space/api"
|
||||
},
|
||||
"timeout": 3600000
|
||||
},
|
||||
"/testnet/api/v1/ws": {
|
||||
"target": "https://mempool.space/testnet",
|
||||
"secure": false,
|
||||
"ws": true,
|
||||
"loglevel": "debug",
|
||||
"pathRewrite": {
|
||||
"^/testnet/api": "/api/v1/ws"
|
||||
}
|
||||
},
|
||||
"/testnet/api/*": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": true,
|
||||
"changeOrigin": true,
|
||||
"loglevel": "debug",
|
||||
"pathRewrite": {
|
||||
"/testnet/api": "/testnet/api"
|
||||
},
|
||||
"timeout": 3600000
|
||||
},
|
||||
"/signet/api/v1/ws": {
|
||||
"target": "https://mempool.space/signet",
|
||||
"secure": false,
|
||||
"ws": true,
|
||||
"loglevel": "debug",
|
||||
"pathRewrite": {
|
||||
"^/signet/api": "/api/v1/ws"
|
||||
}
|
||||
},
|
||||
"/signet/api/*": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": true,
|
||||
"changeOrigin": true,
|
||||
"loglevel": "debug",
|
||||
"pathRewrite": {
|
||||
"/signet/api": "/signet/api"
|
||||
},
|
||||
"timeout": 3600000
|
||||
},
|
||||
|
||||
"/bisq/api/v1/ws": {
|
||||
"target": "https://mempool.space/bisq",
|
||||
"secure": false,
|
||||
"ws": true,
|
||||
"pathRewrite": {
|
||||
"^/bisq/api": "/api/v1/ws"
|
||||
}
|
||||
},
|
||||
"/bisq/api/*": {
|
||||
"target": "https://mempool.space/bisq",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"pathRewrite": {
|
||||
"^/bisq/api/": "/api/v1/bisq/"
|
||||
},
|
||||
"timeout": 3600000
|
||||
},
|
||||
"/liquid/api/v1/ws": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"ws": true
|
||||
},
|
||||
"/liquid/api/*": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"pathRewrite": {
|
||||
"^/liquid/api/": "/liquid/api/"
|
||||
},
|
||||
"timeout": 3600000
|
||||
},
|
||||
"/resources/assets.minimal.json": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
},
|
||||
"/resources/assets.json": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
},
|
||||
"/resources/pools.json": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
}
|
||||
}
|
||||
100
frontend/proxy.stg.conf.json
Normal file
100
frontend/proxy.stg.conf.json
Normal file
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"/api/v1/ws": {
|
||||
"target": "https://mempool.ninja",
|
||||
"secure": false,
|
||||
"ws": true
|
||||
},
|
||||
"/api/*": {
|
||||
"target": "https://mempool.ninja",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"logLevel": "debug",
|
||||
"pathRewrite": {
|
||||
"^/api": "https://mempool.ninja/api"
|
||||
},
|
||||
"timeout": 3600000
|
||||
},
|
||||
"/testnet/api/v1/ws": {
|
||||
"target": "https://mempool.ninja/testnet",
|
||||
"secure": false,
|
||||
"ws": true,
|
||||
"pathRewrite": {
|
||||
"^/testnet/api": "/api/v1/ws"
|
||||
}
|
||||
},
|
||||
"/testnet/api/v1/*": {
|
||||
"target": "https://mempool.ninja/testnet",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"pathRewrite": {
|
||||
"^/testnet/api/v1": "/api/v1"
|
||||
},
|
||||
"timeout": 3600000
|
||||
},
|
||||
"/signet/api/v1/ws": {
|
||||
"target": "https://mempool.ninja/signet",
|
||||
"secure": false,
|
||||
"ws": true,
|
||||
"pathRewrite": {
|
||||
"^/signet/api": "/api/v1/ws"
|
||||
}
|
||||
},
|
||||
"/signet/api/v1/*": {
|
||||
"target": "https://mempool.ninja/signet",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"pathRewrite": {
|
||||
"^/signet/api/v1": "/api/v1"
|
||||
},
|
||||
"timeout": 3600000
|
||||
},
|
||||
"/bisq/api/v1/ws": {
|
||||
"target": "https://mempool.ninja/bisq",
|
||||
"secure": false,
|
||||
"ws": true,
|
||||
"pathRewrite": {
|
||||
"^/bisq/api": "/api/v1/ws"
|
||||
}
|
||||
},
|
||||
"/bisq/api/*": {
|
||||
"target": "https://mempool.ninja/bisq",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"pathRewrite": {
|
||||
"^/bisq/api/": "/api/v1/bisq/"
|
||||
},
|
||||
"timeout": 3600000
|
||||
},
|
||||
"/liquid/api/v1/ws": {
|
||||
"target": "https://mempool.ninja/liquid",
|
||||
"secure": false,
|
||||
"ws": true,
|
||||
"pathRewrite": {
|
||||
"^/liquid/api": "/api/v1/ws"
|
||||
}
|
||||
},
|
||||
"/liquid/api/*": {
|
||||
"target": "https://mempool.ninja/liquid",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"pathRewrite": {
|
||||
"^/liquid/api/": "/api/v1/liquid/"
|
||||
},
|
||||
"timeout": 3600000
|
||||
},
|
||||
"/resources/assets.minimal.json": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
},
|
||||
"/resources/assets.json": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
},
|
||||
"/resources/pools.json": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,10 @@ import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component';
|
||||
import { ApiDocsComponent } from './components/api-docs/api-docs.component';
|
||||
import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component';
|
||||
import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
|
||||
import { SponsorComponent } from './components/sponsor/sponsor.component';
|
||||
|
||||
const routes: Routes = [
|
||||
let routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: MasterPageComponent,
|
||||
@@ -69,6 +71,10 @@ const routes: Routes = [
|
||||
children: [],
|
||||
component: AddressComponent
|
||||
},
|
||||
{
|
||||
path: 'sponsor',
|
||||
component: SponsorComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -283,9 +289,23 @@ const routes: Routes = [
|
||||
},
|
||||
];
|
||||
|
||||
const browserWindow = window || {};
|
||||
// @ts-ignore
|
||||
const browserWindowEnv = browserWindow.__env || {};
|
||||
|
||||
if (browserWindowEnv && browserWindowEnv.OFFICIAL_BISQ_MARKETS) {
|
||||
routes = [{
|
||||
path: '',
|
||||
component: BisqMasterPageComponent,
|
||||
loadChildren: () => import('./bisq/bisq.module').then(m => m.BisqModule)
|
||||
}];
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes, {
|
||||
initialNavigation: 'enabled'
|
||||
initialNavigation: 'enabled',
|
||||
scrollPositionRestoration: 'enabled',
|
||||
anchorScrolling: 'enabled'
|
||||
})],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
|
||||
@@ -71,11 +71,11 @@ export const languages: Language[] = [
|
||||
{ code: 'ja', name: '日本語' }, // Japanese
|
||||
{ code: 'nb', name: 'Norsk' }, // Norwegian Bokmål
|
||||
// { code: 'nn', name: 'Norsk Nynorsk' }, // Norwegian Nynorsk
|
||||
// { code: 'pl', name: 'Polski' }, // Polish
|
||||
{ code: 'pl', name: 'Polski' }, // Polish
|
||||
{ code: 'pt', name: 'Português' }, // Portuguese
|
||||
// { code: 'pt-BR', name: 'Português (Brazil)' }, // Portuguese (Brazil)
|
||||
// { code: 'ro', name: 'Română' }, // Romanian
|
||||
// { code: 'ru', name: 'Русский' }, // Russian
|
||||
{ code: 'ru', name: 'Русский' }, // Russian
|
||||
// { code: 'sk', name: 'Slovenčina' }, // Slovak
|
||||
{ code: 'sl', name: 'Slovenščina' }, // Slovenian
|
||||
// { code: 'sr', name: 'Српски / srpski' }, // Serbian
|
||||
|
||||
@@ -21,6 +21,7 @@ import { WebsocketService } from './services/websocket.service';
|
||||
import { AddressLabelsComponent } from './components/address-labels/address-labels.component';
|
||||
import { MempoolBlocksComponent } from './components/mempool-blocks/mempool-blocks.component';
|
||||
import { MasterPageComponent } from './components/master-page/master-page.component';
|
||||
import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
|
||||
import { AboutComponent } from './components/about/about.component';
|
||||
import { TelevisionComponent } from './components/television/television.component';
|
||||
import { StatisticsComponent } from './components/statistics/statistics.component';
|
||||
@@ -44,17 +45,20 @@ import { FeesBoxComponent } from './components/fees-box/fees-box.component';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
||||
import { faAngleDown, faAngleUp, faBolt, faChartArea, faCogs, faCubes, faDatabase, faExchangeAlt, faInfoCircle,
|
||||
faLink, faList, faSearch, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faAngleDoubleUp } from '@fortawesome/free-solid-svg-icons';
|
||||
faLink, faList, faSearch, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faAngleDoubleUp, faChevronDown, faFileAlt, faRedoAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { ApiDocsComponent } from './components/api-docs/api-docs.component';
|
||||
import { CodeTemplateComponent } from './components/api-docs/code-template.component';
|
||||
import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component';
|
||||
import { StorageService } from './services/storage.service';
|
||||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||
import { SponsorComponent } from './components/sponsor/sponsor.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
AboutComponent,
|
||||
MasterPageComponent,
|
||||
BisqMasterPageComponent,
|
||||
TelevisionComponent,
|
||||
BlockchainComponent,
|
||||
StartComponent,
|
||||
@@ -82,7 +86,9 @@ import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||
FeesBoxComponent,
|
||||
DashboardComponent,
|
||||
ApiDocsComponent,
|
||||
CodeTemplateComponent,
|
||||
TermsOfServiceComponent,
|
||||
SponsorComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule.withServerTransition({ appId: 'serverApp' }),
|
||||
@@ -127,5 +133,8 @@ export class AppModule {
|
||||
library.addIcons(faExchangeAlt);
|
||||
library.addIcons(faAngleDoubleUp);
|
||||
library.addIcons(faAngleDoubleDown);
|
||||
library.addIcons(faChevronDown);
|
||||
library.addIcons(faFileAlt);
|
||||
library.addIcons(faRedoAlt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<div class="container-xl">
|
||||
<h1 style="float: left;" i18n="shared.address">Address</h1>
|
||||
<a [routerLink]="['/address/' | relativeUrl, addressString]" style="line-height: 56px; margin-left: 10px;">
|
||||
<span class="d-inline d-lg-none">{{ addressString | shortenString : 24 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ addressString }}</span>
|
||||
</a>
|
||||
<app-clipboard [text]="addressString"></app-clipboard>
|
||||
<h1 i18n="shared.address">Address</h1>
|
||||
<span class="address-link">
|
||||
<a [routerLink]="['/address/' | relativeUrl, addressString]">
|
||||
<span class="d-inline d-lg-none">{{ addressString | shortenString : 24 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ addressString }}</span>
|
||||
</a>
|
||||
<app-clipboard [text]="addressString"></app-clipboard>
|
||||
</span>
|
||||
<br>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
@@ -13,26 +15,26 @@
|
||||
<div class="box">
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="col-md">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="address.total-received">Total received</td>
|
||||
<td>{{ totalReceived / 100 | number: '1.2-2' }} BSQ</td>
|
||||
<td>{{ totalReceived / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="address.total-sent">Total sent</td>
|
||||
<td>{{ totalSent / 100 | number: '1.2-2' }} BSQ</td>
|
||||
<td>{{ totalSent / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="address.balance">Balance</td>
|
||||
<td>{{ (totalReceived - totalSent) / 100 | number: '1.2-2' }} BSQ (<app-bsq-amount [bsq]="totalReceived - totalSent" [forceFiat]="true" [green]="true"></app-bsq-amount>)</td>
|
||||
<td>{{ (totalReceived - totalSent) / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span> <span class="fiat"><app-bsq-amount [bsq]="totalReceived - totalSent" [forceFiat]="true" [green]="true"></app-bsq-amount></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="w-100 d-block d-md-none"></div>
|
||||
<div class="col qrcode-col">
|
||||
<div class="col-md qrcode-col">
|
||||
<div class="qr-wrapper">
|
||||
<app-qrcode [data]="addressString"></app-qrcode>
|
||||
</div>
|
||||
|
||||
@@ -3,21 +3,73 @@
|
||||
padding: 10px;
|
||||
padding-bottom: 5px;
|
||||
display: inline-block;
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.qrcode-col {
|
||||
text-align: right;
|
||||
}
|
||||
.qrcode-col {
|
||||
text-align: center;
|
||||
}
|
||||
@media (max-width: 575.98px) {
|
||||
.qrcode-col {
|
||||
|
||||
.qrcode-col > div {
|
||||
margin: 20px auto 5px;
|
||||
@media (min-width: 768px) {
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.qrcode-col > div {
|
||||
margin-top: 20px;
|
||||
margin-right: 0px;
|
||||
.fiat {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
@media (min-width: 768px) {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.table {
|
||||
tr td {
|
||||
&:last-child {
|
||||
text-align: right;
|
||||
@media (min-width: 768px) {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
@media (min-width: 576px) {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.address-link {
|
||||
line-height: 26px;
|
||||
margin-left: 0px;
|
||||
top: 14px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@media (min-width: 768px) {
|
||||
line-height: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
.row{
|
||||
flex-direction: column;
|
||||
@media (min-width: 576px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.mobile-bottomcol {
|
||||
margin-top: 15px;
|
||||
}
|
||||
.details-table td:first-child {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { ParamMap, ActivatedRoute } from '@angular/router';
|
||||
import { Subscription, of } from 'rxjs';
|
||||
import { BisqTransaction } from '../bisq.interfaces';
|
||||
import { BisqApiService } from '../bisq-api.service';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-address',
|
||||
@@ -22,12 +23,15 @@ export class BisqAddressComponent implements OnInit, OnDestroy {
|
||||
totalSent = 0;
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private route: ActivatedRoute,
|
||||
private seoService: SeoService,
|
||||
private bisqApiService: BisqApiService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.mainSubscription = this.route.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpResponse, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BisqTransaction, BisqBlock, BisqStats } from './bisq.interfaces';
|
||||
import { BisqTransaction, BisqBlock, BisqStats, MarketVolume, Trade, Markets, Tickers, Offers, Currencies, HighLowOpenClose, SummarizedInterval } from './bisq.interfaces';
|
||||
|
||||
const API_BASE_URL = '/bisq/api';
|
||||
|
||||
@@ -42,4 +42,37 @@ export class BisqApiService {
|
||||
getAddress$(address: string): Observable<BisqTransaction[]> {
|
||||
return this.httpClient.get<BisqTransaction[]>(API_BASE_URL + '/address/' + address);
|
||||
}
|
||||
|
||||
getMarkets$(): Observable<Markets> {
|
||||
return this.httpClient.get<Markets>(API_BASE_URL + '/markets/markets');
|
||||
}
|
||||
|
||||
getMarketsTicker$(): Observable<Tickers> {
|
||||
return this.httpClient.get<Tickers>(API_BASE_URL + '/markets/ticker');
|
||||
}
|
||||
|
||||
getMarketsCurrencies$(): Observable<Currencies> {
|
||||
return this.httpClient.get<Currencies>(API_BASE_URL + '/markets/currencies');
|
||||
}
|
||||
|
||||
getMarketsHloc$(market: string, interval: 'minute' | 'half_hour' | 'hour' | 'half_day' | 'day'
|
||||
| 'week' | 'month' | 'year' | 'auto'): Observable<SummarizedInterval[]> {
|
||||
return this.httpClient.get<SummarizedInterval[]>(API_BASE_URL + '/markets/hloc?market=' + market + '&interval=' + interval);
|
||||
}
|
||||
|
||||
getMarketOffers$(market: string): Observable<Offers> {
|
||||
return this.httpClient.get<Offers>(API_BASE_URL + '/markets/offers?market=' + market);
|
||||
}
|
||||
|
||||
getMarketTrades$(market: string): Observable<Trade[]> {
|
||||
return this.httpClient.get<Trade[]>(API_BASE_URL + '/markets/trades?market=' + market);
|
||||
}
|
||||
|
||||
getMarketVolumesByTime$(period: string): Observable<HighLowOpenClose[]> {
|
||||
return this.httpClient.get<HighLowOpenClose[]>(API_BASE_URL + '/markets/volumes/' + period);
|
||||
}
|
||||
|
||||
getAllVolumesDay$(): Observable<MarketVolume[]> {
|
||||
return this.httpClient.get<MarketVolume[]>(API_BASE_URL + '/markets/volumes?interval=week');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
<ng-template [ngIf]="!isLoading && !error">
|
||||
|
||||
<div class="box">
|
||||
<div class="box block-container">
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
@@ -22,13 +22,13 @@
|
||||
<td>
|
||||
{{ block.time | date:'yyyy-MM-dd HH:mm' }}
|
||||
<div class="lg-inline">
|
||||
<i>(<app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since>)</i>
|
||||
<i class="symbol">(<app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since>)</i>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
@@ -73,7 +73,7 @@
|
||||
<ng-template [ngIf]="isLoading && !error">
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
@@ -86,7 +86,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
|
||||
@@ -1,10 +1,44 @@
|
||||
|
||||
.td-width {
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.td-width {
|
||||
width: 140px;
|
||||
width: 140px;
|
||||
@media (min-width: 768px) {
|
||||
width: 175px;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
@media (min-width: 576px) {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.row{
|
||||
flex-direction: column;
|
||||
@media (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.block-container {
|
||||
.table {
|
||||
tr td {
|
||||
&:last-child {
|
||||
text-align: right;
|
||||
@media (min-width: 992px) {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.fiat {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
@media (min-width: 992px) {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { switchMap, catchError } from 'rxjs/operators';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { ElectrsApiService } from 'src/app/services/electrs-api.service';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-block',
|
||||
@@ -23,6 +24,7 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
|
||||
error: HttpErrorResponse | null;
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private bisqApiService: BisqApiService,
|
||||
private route: ActivatedRoute,
|
||||
private seoService: SeoService,
|
||||
@@ -32,6 +34,8 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.subscription = this.route.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="container-xl">
|
||||
<h1 style="float: left;" i18n="Bisq blocks header">Blocks</h1>
|
||||
<h1 style="float: left;" i18n="Bisq blocks header">BSQ Blocks</h1>
|
||||
<br>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
@@ -18,7 +18,7 @@
|
||||
<tr *ngFor="let block of blocks.value[0]; trackBy: trackByFn">
|
||||
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
|
||||
<td><app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since></td>
|
||||
<td>{{ calculateTotalOutput(block) / 100 | number: '1.2-2' }}<span class="d-none d-md-inline"> BSQ</span></td>
|
||||
<td>{{ calculateTotalOutput(block) / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
|
||||
<td class="d-none d-md-block">{{ block.txs.length }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -26,9 +26,9 @@
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<ngb-pagination *ngIf="blocks.value" [size]="paginationSize" [collectionSize]="blocks.value[1]" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true"></ngb-pagination>
|
||||
|
||||
<div class="pagination">
|
||||
<ngb-pagination *ngIf="blocks.value" [size]="paginationSize" [collectionSize]="blocks.value[1]" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true"></ngb-pagination>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.pagination {
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { Observable } from 'rxjs';
|
||||
import { BisqBlock, BisqOutput, BisqTransaction } from '../bisq.interfaces';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-blocks',
|
||||
@@ -22,9 +23,10 @@ export class BisqBlocksComponent implements OnInit {
|
||||
isLoading = true;
|
||||
// @ts-ignore
|
||||
paginationSize: 'sm' | 'lg' = 'md';
|
||||
paginationMaxSize = 10;
|
||||
paginationMaxSize = 4;
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private bisqApiService: BisqApiService,
|
||||
private seoService: SeoService,
|
||||
private route: ActivatedRoute,
|
||||
@@ -32,6 +34,7 @@ export class BisqBlocksComponent implements OnInit {
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks']);
|
||||
this.seoService.setTitle($localize`:@@8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`);
|
||||
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
|
||||
this.loadingItems = Array(this.itemsPerPage);
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<div class="container-xl">
|
||||
|
||||
<h1 i18n="Bisq markets title">Bisq Trading Volume</h1>
|
||||
|
||||
<div id="volumeHolder">
|
||||
<ng-template #loadingVolumes>
|
||||
<div class="text-center loadingVolumes">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="volumes$ | async as volumes; else loadingVolumes">
|
||||
<app-lightweight-charts-area [data]="volumes.data" [lineData]="volumes.linesData"></app-lightweight-charts-area>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<br><br>
|
||||
|
||||
<div class="container-info">
|
||||
<h1>
|
||||
<ng-template [ngIf]="stateService.env.OFFICIAL_BISQ_MARKETS" [ngIfElse]="nonOfficialMarkets" i18n="Bisq All Markets">Markets</ng-template>
|
||||
<ng-template #nonOfficialMarkets i18n="Bisq Bitcoin Markets">Bitcoin Markets</ng-template>
|
||||
</h1>
|
||||
<ng-container *ngIf="{ value: (tickers$ | async) } as tickers">
|
||||
<div class="table-container">
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<th><ng-container i18n>Currency</ng-container> <button [disabled]="(sort$ | async) === 'name'" class="btn btn-link btn-sm" (click)="sort('name')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
|
||||
<th i18n>Price</th>
|
||||
<th><ng-container i18n="Trading volume 7D">Volume (7d)</ng-container> <button [disabled]="(sort$ | async) === 'volumes'" class="btn btn-link btn-sm" (click)="sort('volumes')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
|
||||
<th><ng-container i18n="Trades amount 7D">Trades (7d)</ng-container> <button [disabled]="(sort$ | async) === 'trades'" class="btn btn-link btn-sm" (click)="sort('trades')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
|
||||
</thead>
|
||||
<tbody *ngIf="tickers.value; else loadingTmpl">
|
||||
<tr *ngFor="let ticker of tickers.value; trackBy: trackByFn;">
|
||||
<td><a [routerLink]="['/market' | relativeUrl, ticker.pair_url]">{{ ticker.name }})</a></td>
|
||||
<td>
|
||||
<app-fiat *ngIf="ticker.market.rtype === 'crypto'; else fiat" [value]="ticker.last * 100000000"></app-fiat>
|
||||
<ng-template #fiat>
|
||||
<span class="green-color">{{ ticker.last | currency: ticker.market.rsymbol }}</span>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td>
|
||||
<app-fiat [value]="ticker.volume?.volume"></app-fiat>
|
||||
</td>
|
||||
<td>{{ ticker.volume?.num_trades ? ticker.volume?.num_trades : 0 }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<br><br>
|
||||
|
||||
<h2 i18n="Latest Trades header">Latest Trades</h2>
|
||||
<app-bisq-trades [trades$]="trades$"></app-bisq-trades>
|
||||
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingTmpl>
|
||||
<tr *ngFor="let i of [1,2,3,4,5,6,7,8,9,10]">
|
||||
<td *ngFor="let j of [1, 2, 3, 4]"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,35 @@
|
||||
#volumeHolder {
|
||||
height: 500px;
|
||||
background-color: #000;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.table {
|
||||
max-width: 100%;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.loadingVolumes {
|
||||
position: relative;
|
||||
top: 45%;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow: scroll;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
font-size: 13px;
|
||||
@media(min-width: 576px){
|
||||
font-size: 16px;
|
||||
}
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.container-info{
|
||||
overflow-x: scroll;
|
||||
}
|
||||
131
frontend/src/app/bisq/bisq-dashboard/bisq-dashboard.component.ts
Normal file
131
frontend/src/app/bisq/bisq-dashboard/bisq-dashboard.component.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { Observable, combineLatest, BehaviorSubject, of } from 'rxjs';
|
||||
import { map, share, switchMap } from 'rxjs/operators';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
import { BisqApiService } from '../bisq-api.service';
|
||||
import { Trade } from '../bisq.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-dashboard',
|
||||
templateUrl: './bisq-dashboard.component.html',
|
||||
styleUrls: ['./bisq-dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class BisqDashboardComponent implements OnInit {
|
||||
tickers$: Observable<any>;
|
||||
volumes$: Observable<any>;
|
||||
trades$: Observable<Trade[]>;
|
||||
sort$ = new BehaviorSubject<string>('trades');
|
||||
|
||||
allowCryptoCoins = ['usdc', 'l-btc', 'bsq'];
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private bisqApiService: BisqApiService,
|
||||
public stateService: StateService,
|
||||
private seoService: SeoService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle(`Markets`);
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.volumes$ = this.bisqApiService.getAllVolumesDay$()
|
||||
.pipe(
|
||||
map((volumes) => {
|
||||
const data = volumes.map((volume) => {
|
||||
return {
|
||||
time: volume.period_start,
|
||||
value: volume.volume,
|
||||
};
|
||||
});
|
||||
|
||||
const linesData = volumes.map((volume) => {
|
||||
return {
|
||||
time: volume.period_start,
|
||||
value: volume.num_trades,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
data: data,
|
||||
linesData: linesData,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const getMarkets = this.bisqApiService.getMarkets$().pipe(share());
|
||||
|
||||
this.tickers$ = combineLatest([
|
||||
this.bisqApiService.getMarketsTicker$(),
|
||||
getMarkets,
|
||||
this.bisqApiService.getMarketVolumesByTime$('7d'),
|
||||
])
|
||||
.pipe(
|
||||
map(([tickers, markets, volumes]) => {
|
||||
|
||||
const newTickers = [];
|
||||
for (const t in tickers) {
|
||||
|
||||
if (!this.stateService.env.OFFICIAL_BISQ_MARKETS) {
|
||||
const pair = t.split('_');
|
||||
if (pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const mappedTicker: any = tickers[t];
|
||||
|
||||
mappedTicker.pair_url = t;
|
||||
mappedTicker.pair = t.replace('_', '/').toUpperCase();
|
||||
mappedTicker.market = markets[t];
|
||||
mappedTicker.volume = volumes[t];
|
||||
mappedTicker.name = `${mappedTicker.market.rtype === 'crypto' ? mappedTicker.market.lname : mappedTicker.market.rname} (${mappedTicker.market.rtype === 'crypto' ? mappedTicker.market.lsymbol : mappedTicker.market.rsymbol}`;
|
||||
newTickers.push(mappedTicker);
|
||||
}
|
||||
return newTickers;
|
||||
}),
|
||||
switchMap((tickers) => combineLatest([this.sort$, of(tickers)])),
|
||||
map(([sort, tickers]) => {
|
||||
if (sort === 'trades') {
|
||||
tickers.sort((a, b) => (b.volume && b.volume.num_trades || 0) - (a.volume && a.volume.num_trades || 0));
|
||||
} else if (sort === 'volumes') {
|
||||
tickers.sort((a, b) => (b.volume && b.volume.volume || 0) - (a.volume && a.volume.volume || 0));
|
||||
} else if (sort === 'name') {
|
||||
tickers.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
return tickers;
|
||||
})
|
||||
);
|
||||
|
||||
this.trades$ = combineLatest([
|
||||
this.bisqApiService.getMarketTrades$('all'),
|
||||
getMarkets,
|
||||
])
|
||||
.pipe(
|
||||
map(([trades, markets]) => {
|
||||
if (!this.stateService.env.OFFICIAL_BISQ_MARKETS) {
|
||||
trades = trades.filter((trade) => {
|
||||
const pair = trade.market.split('_');
|
||||
return !(pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1);
|
||||
});
|
||||
}
|
||||
return trades.map((trade => {
|
||||
trade._market = markets[trade.market];
|
||||
return trade;
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
trackByFn(index: number) {
|
||||
return index;
|
||||
}
|
||||
|
||||
sort(by: string) {
|
||||
this.sort$.next(by);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<router-outlet></router-outlet>
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-explorer',
|
||||
templateUrl: './bisq-explorer.component.html',
|
||||
styleUrls: ['./bisq-explorer.component.scss']
|
||||
})
|
||||
export class BisqExplorerComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<div class="container-xl">
|
||||
|
||||
<br>
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2">
|
||||
<div class="col mb-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title" i18n="bisq-dashboard.price-index-title">Bisq Price Index</h5>
|
||||
<div class="big-fiat">
|
||||
<span *ngIf="usdPrice$ | async as usdPrice; else loading">
|
||||
<span [appColoredPrice]="usdPrice">{{ usdPrice | currency:'USD':'symbol':'1.2-2' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col mb-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title" i18n="bisq-dashboard.market-price-title">Bisq Market Price</h5>
|
||||
<div class="big-fiat">
|
||||
<span class="green-color" *ngIf="bisqMarketPrice; else loading">
|
||||
<span [appColoredPrice]="bisqMarketPrice">{{ bisqMarketPrice | currency:'USD':'symbol':'1.2-2' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2">
|
||||
<div class="col mb-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">US Dollar - BTC/USD</h5>
|
||||
<div class="chart-container">
|
||||
<ng-container *ngIf="hlocData$ | async as hlocData; else loadingSpinner">
|
||||
<app-lightweight-charts [height]="300" [data]="hlocData.hloc" [volumeData]="hlocData.volume" [precision]="2"></app-lightweight-charts>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col mb-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title" i18n="Bisq markets title">Bisq Trading Volume</h5>
|
||||
<div class="chart-container">
|
||||
<ng-container *ngIf="volumes$ | async as volumes; else loadingSpinner">
|
||||
<app-lightweight-charts-area [height]="300" [data]="volumes.data" [lineData]="volumes.linesData"></app-lightweight-charts-area>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2">
|
||||
<ng-container *ngIf="{ value: (tickers$ | async) } as tickers">
|
||||
|
||||
<div class="col mb-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-center">
|
||||
<ng-template [ngIf]="stateService.env.OFFICIAL_BISQ_MARKETS" [ngIfElse]="nonOfficialMarkets" i18n="Bisq All Markets">Markets</ng-template>
|
||||
<ng-template #nonOfficialMarkets i18n="Bisq Bitcoin Markets">Bitcoin Markets</ng-template>
|
||||
</h5>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<th><ng-container i18n>Currency</ng-container> <button [disabled]="(sort$ | async) === 'name'" class="btn btn-link btn-sm" (click)="sort('name')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
|
||||
<th i18n>Price</th>
|
||||
<th><ng-container i18n="Trades amount 7D">Trades (7d)</ng-container> <button [disabled]="(sort$ | async) === 'trades'" class="btn btn-link btn-sm" (click)="sort('trades')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
|
||||
</thead>
|
||||
<tbody *ngIf="tickers.value; else loadingTmpl">
|
||||
<tr *ngFor="let ticker of tickers.value; trackBy: trackByFn;">
|
||||
<td><a [routerLink]="['/market' | relativeUrl, ticker.pair_url]">{{ ticker.name }})</a></td>
|
||||
<td>
|
||||
<app-fiat *ngIf="ticker.market.rtype === 'crypto'; else fiat" [value]="ticker.last * 100000000"></app-fiat>
|
||||
<ng-template #fiat>
|
||||
<span class="green-color">{{ ticker.last | currency: ticker.market.rsymbol }}</span>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td>{{ ticker.volume?.num_trades ? ticker.volume?.num_trades : 0 }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="text-center"><a href="" [routerLink]="['/markets' | relativeUrl]" i18n="dashboard.view-all">View all »</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col mb-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-center" i18n="Latest Trades header">Latest Trades</h5>
|
||||
<app-bisq-trades [trades$]="trades$" view="small"></app-bisq-trades>
|
||||
<div class="text-center"><a href="" [routerLink]="['/markets' | relativeUrl]" i18n="dashboard.view-all">View all »</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<app-language-selector *ngIf="!stateService.env.OFFICIAL_BISQ_MARKETS"></app-language-selector>
|
||||
|
||||
<div class="text-small text-center mt-3">
|
||||
<a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<ng-template #loadingTmpl>
|
||||
<tr *ngFor="let i of [1,2,3,4,5,6,7,8,9,10]">
|
||||
<td *ngFor="let j of [1, 2, 3]"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #loadingSpinner>
|
||||
<div class="text-center loadingGraphs">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #loading>
|
||||
<div class="skeleton-loader shorter"></div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,84 @@
|
||||
#volumeHolder {
|
||||
height: 500px;
|
||||
background-color: #000;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.table {
|
||||
max-width: 100%;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.loadingGraphs {
|
||||
position: relative;
|
||||
top: 45%;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
font-size: 13px;
|
||||
@media(min-width: 576px){
|
||||
font-size: 16px;
|
||||
}
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.big-fiat {
|
||||
color: #3bcc49;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
|
||||
.card {
|
||||
background-color: #1d1f31;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: #4a68b9;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.info-block {
|
||||
float: left;
|
||||
width: 350px;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
background-color: #2d3348;
|
||||
height: 1.1rem;
|
||||
}
|
||||
|
||||
.bg-warning {
|
||||
background-color: #b58800 !important;
|
||||
}
|
||||
|
||||
.skeleton-loader {
|
||||
max-width: 100%;
|
||||
&.shorter {
|
||||
max-width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.more-padding {
|
||||
padding: 1.25rem 2rem 1.25rem 2rem;
|
||||
}
|
||||
|
||||
.graph-card {
|
||||
height: 100%;
|
||||
@media (min-width: 992px) {
|
||||
height: 385px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { Observable, combineLatest, BehaviorSubject, of } from 'rxjs';
|
||||
import { map, share, switchMap } from 'rxjs/operators';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
import { BisqApiService } from '../bisq-api.service';
|
||||
import { Trade } from '../bisq.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'app-main-bisq-dashboard',
|
||||
templateUrl: './bisq-main-dashboard.component.html',
|
||||
styleUrls: ['./bisq-main-dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class BisqMainDashboardComponent implements OnInit {
|
||||
tickers$: Observable<any>;
|
||||
volumes$: Observable<any>;
|
||||
trades$: Observable<Trade[]>;
|
||||
sort$ = new BehaviorSubject<string>('trades');
|
||||
hlocData$: Observable<any>;
|
||||
usdPrice$: Observable<number>;
|
||||
isLoadingGraph = true;
|
||||
bisqMarketPrice = 0;
|
||||
|
||||
allowCryptoCoins = ['usdc', 'l-btc', 'bsq'];
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private bisqApiService: BisqApiService,
|
||||
public stateService: StateService,
|
||||
private seoService: SeoService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle(`Markets`);
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.usdPrice$ = this.stateService.conversions$.asObservable().pipe(
|
||||
map((conversions) => conversions.USD)
|
||||
);
|
||||
|
||||
this.volumes$ = this.bisqApiService.getAllVolumesDay$()
|
||||
.pipe(
|
||||
map((volumes) => {
|
||||
const data = volumes.map((volume) => {
|
||||
return {
|
||||
time: volume.period_start,
|
||||
value: volume.volume,
|
||||
};
|
||||
});
|
||||
|
||||
const linesData = volumes.map((volume) => {
|
||||
return {
|
||||
time: volume.period_start,
|
||||
value: volume.num_trades,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
data: data,
|
||||
linesData: linesData,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const getMarkets = this.bisqApiService.getMarkets$().pipe(share());
|
||||
|
||||
this.tickers$ = combineLatest([
|
||||
this.bisqApiService.getMarketsTicker$(),
|
||||
getMarkets,
|
||||
this.bisqApiService.getMarketVolumesByTime$('7d'),
|
||||
])
|
||||
.pipe(
|
||||
map(([tickers, markets, volumes]) => {
|
||||
|
||||
const newTickers = [];
|
||||
for (const t in tickers) {
|
||||
|
||||
if (!this.stateService.env.OFFICIAL_BISQ_MARKETS) {
|
||||
const pair = t.split('_');
|
||||
if (pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const mappedTicker: any = tickers[t];
|
||||
|
||||
mappedTicker.pair_url = t;
|
||||
mappedTicker.pair = t.replace('_', '/').toUpperCase();
|
||||
mappedTicker.market = markets[t];
|
||||
mappedTicker.volume = volumes[t];
|
||||
mappedTicker.name = `${mappedTicker.market.rtype === 'crypto' ? mappedTicker.market.lname : mappedTicker.market.rname} (${mappedTicker.market.rtype === 'crypto' ? mappedTicker.market.lsymbol : mappedTicker.market.rsymbol}`;
|
||||
newTickers.push(mappedTicker);
|
||||
}
|
||||
return newTickers;
|
||||
}),
|
||||
switchMap((tickers) => combineLatest([this.sort$, of(tickers)])),
|
||||
map(([sort, tickers]) => {
|
||||
if (sort === 'trades') {
|
||||
tickers.sort((a, b) => (b.volume && b.volume.num_trades || 0) - (a.volume && a.volume.num_trades || 0));
|
||||
} else if (sort === 'volumes') {
|
||||
tickers.sort((a, b) => (b.volume && b.volume.volume || 0) - (a.volume && a.volume.volume || 0));
|
||||
} else if (sort === 'name') {
|
||||
tickers.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
return tickers.slice(0, 10);
|
||||
})
|
||||
);
|
||||
|
||||
this.trades$ = combineLatest([
|
||||
this.bisqApiService.getMarketTrades$('all'),
|
||||
getMarkets,
|
||||
])
|
||||
.pipe(
|
||||
map(([trades, markets]) => {
|
||||
if (!this.stateService.env.OFFICIAL_BISQ_MARKETS) {
|
||||
trades = trades.filter((trade) => {
|
||||
const pair = trade.market.split('_');
|
||||
return !(pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1);
|
||||
});
|
||||
}
|
||||
return trades.map((trade => {
|
||||
trade._market = markets[trade.market];
|
||||
return trade;
|
||||
})).slice(0, 10);
|
||||
})
|
||||
);
|
||||
|
||||
this.hlocData$ = this.bisqApiService.getMarketsHloc$('btc_usd', 'day')
|
||||
.pipe(
|
||||
map((hlocData) => {
|
||||
this.isLoadingGraph = false;
|
||||
|
||||
hlocData = hlocData.map((h) => {
|
||||
h.time = h.period_start;
|
||||
return h;
|
||||
});
|
||||
|
||||
const hlocVolume = hlocData.map((h) => {
|
||||
return {
|
||||
time: h.time,
|
||||
value: h.volume_right,
|
||||
color: h.close > h.avg ? 'rgba(0, 41, 74, 0.7)' : 'rgba(0, 41, 74, 1)',
|
||||
};
|
||||
});
|
||||
|
||||
// Add whitespace
|
||||
if (hlocData.length > 1) {
|
||||
const newHloc = [];
|
||||
newHloc.push(hlocData[0]);
|
||||
|
||||
const period = 86400;
|
||||
let periods = 0;
|
||||
const startingDate = hlocData[0].period_start;
|
||||
let index = 1;
|
||||
while (true) {
|
||||
periods++;
|
||||
if (hlocData[index].period_start > startingDate + period * periods) {
|
||||
newHloc.push({
|
||||
time: startingDate + period * periods,
|
||||
});
|
||||
} else {
|
||||
newHloc.push(hlocData[index]);
|
||||
index++;
|
||||
if (!hlocData[index]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
hlocData = newHloc;
|
||||
}
|
||||
|
||||
this.bisqMarketPrice = hlocData[hlocData.length - 1].close;
|
||||
|
||||
return {
|
||||
hloc: hlocData,
|
||||
volume: hlocVolume,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
trackByFn(index: number) {
|
||||
return index;
|
||||
}
|
||||
|
||||
sort(by: string) {
|
||||
this.sort$.next(by);
|
||||
}
|
||||
|
||||
}
|
||||
112
frontend/src/app/bisq/bisq-market/bisq-market.component.html
Normal file
112
frontend/src/app/bisq/bisq-market/bisq-market.component.html
Normal file
@@ -0,0 +1,112 @@
|
||||
<div class="container-xl">
|
||||
|
||||
<ng-container *ngIf="hlocData$ | async as hlocData; else loadingSpinner">
|
||||
|
||||
<ng-container *ngIf="currency$ | async as currency; else loadingSpinner">
|
||||
<h1>{{ currency.market.rtype === 'crypto' ? currency.market.lname : currency.market.rname }} - {{ currency.pair }}</h1>
|
||||
<div class="priceheader">
|
||||
<ng-container *ngIf="currency.market.rtype === 'fiat'; else headerPriceCrypto"><span class="green-color">{{ hlocData.hloc[hlocData.hloc.length - 1].close | currency: currency.market.rsymbol }}</span></ng-container>
|
||||
<ng-template #headerPriceCrypto>{{ hlocData.hloc[hlocData.hloc.length - 1].close | number: '1.' + currency.market.rprecision + '-' + currency.market.rprecision }} {{ currency.market.rsymbol }}</ng-template>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="radioGroupForm" class="mb-3 radio-form">
|
||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="interval">
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'half_hour'" (click)="setFragment('half_hour')"> 30M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'hour'" (click)="setFragment('hour')"> 1H
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'half_day'" (click)="setFragment('half_day')"> 12H
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'day'" (click)="setFragment('day')"> 1D
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'week'" (click)="setFragment('week')"> 1W
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'month'" (click)="setFragment('month')"> 1M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'year'" (click)="setFragment('year')"> 1Y
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div id="graphHolder">
|
||||
<div class="text-center loadingChart" [hidden]="!isLoadingGraph">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
<app-lightweight-charts [data]="hlocData.hloc" [volumeData]="hlocData.volume" [precision]="currency.market.rtype === 'crypto' ? currency.market.lprecision : currency.market.rprecision"></app-lightweight-charts>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<ng-container *ngIf="offers$ | async as offers; else loadingSpinner">
|
||||
<div class="row row-cols-1 row-cols-md-2">
|
||||
<ng-container *ngTemplateOutlet="offersList; context: { offers: offers.buys, direction: 'BUY', market: currency.market }"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="offersList; context: { offers: offers.sells, direction: 'SELL', market: currency.market }"></ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<br><br>
|
||||
|
||||
<ng-container *ngIf="trades$ | async as trades; else loadingSpinner">
|
||||
<h2 i18n="Latest Trades header">Latest Trades</h2>
|
||||
|
||||
<app-bisq-trades [trades$]="trades$" [market]="currency.market"></app-bisq-trades>
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<ng-template #offersList let-offers="offers" let-direction="direction", let-market="market">
|
||||
<div class="col">
|
||||
<h2>
|
||||
<ng-template [ngIf]="direction === 'BUY'" [ngIfElse]="sellOffers" i18n="Bisq Buy Offers">Buy Offers</ng-template>
|
||||
<ng-template #sellOffers i18n="Bisq Sell Offers">Sell Offers</ng-template>
|
||||
</h2>
|
||||
<div class="table-container">
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<th i18n>Price</th>
|
||||
<th><ng-container *ngTemplateOutlet="amount; context: {$implicit: market.lsymbol }"></ng-container></th>
|
||||
<th><ng-container *ngTemplateOutlet="amount; context: {$implicit: market.rsymbol }"></ng-container></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let offer of offers">
|
||||
<td>
|
||||
<ng-container *ngIf="market.rtype === 'fiat'; else priceCrypto"><span class="green-color">{{ offer.price | currency: market.rsymbol }}</span></ng-container>
|
||||
<ng-template #priceCrypto>{{ offer.price | number: '1.2-' + market.rprecision }} <span class="symbol">{{ market.rsymbol }}</span></ng-template>
|
||||
</td>
|
||||
<td>
|
||||
<ng-container *ngIf="market.ltype === 'fiat'; else amountCrypto"><span class="green-color">{{ offer.amount | currency: market.rsymbol }}</span></ng-container>
|
||||
<ng-template #amountCrypto>{{ offer.amount | number: '1.2-' + market.lprecision }} <span class="symbol">{{ market.lsymbol }}</span></ng-template>
|
||||
</td>
|
||||
<td>
|
||||
<ng-container *ngIf="market.rtype === 'fiat'; else volumeCrypto"><span class="green-color">{{ offer.volume | currency: market.rsymbol }}</span></ng-container>
|
||||
<ng-template #volumeCrypto>{{ offer.volume | number: '1.2-' + market.rprecision }} <span class="symbol">{{ market.rsymbol }}</span></ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #loadingSpinner>
|
||||
<br>
|
||||
<br>
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #amount let-i i18n="Trade amount (Symbol)">Amount ({{ i }})</ng-template>
|
||||
46
frontend/src/app/bisq/bisq-market/bisq-market.component.scss
Normal file
46
frontend/src/app/bisq/bisq-market/bisq-market.component.scss
Normal file
@@ -0,0 +1,46 @@
|
||||
.priceheader {
|
||||
font-size: 24px;
|
||||
@media(min-width: 576px){
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-form {
|
||||
@media(min-width: 576px){
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.loadingChart {
|
||||
z-index: 100;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
#graphHolder {
|
||||
height: 550px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.col {
|
||||
&:last-child{
|
||||
margin-top: 50px;
|
||||
@media(min-width: 576px){
|
||||
margin-top: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow: scroll;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
font-size: 13px;
|
||||
@media(min-width: 576px){
|
||||
font-size: 16px;
|
||||
}
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
158
frontend/src/app/bisq/bisq-market/bisq-market.component.ts
Normal file
158
frontend/src/app/bisq/bisq-market/bisq-market.component.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { combineLatest, merge, Observable, of } from 'rxjs';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
import { BisqApiService } from '../bisq-api.service';
|
||||
import { OffersMarket, Trade } from '../bisq.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-market',
|
||||
templateUrl: './bisq-market.component.html',
|
||||
styleUrls: ['./bisq-market.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class BisqMarketComponent implements OnInit, OnDestroy {
|
||||
hlocData$: Observable<any>;
|
||||
currency$: Observable<any>;
|
||||
offers$: Observable<OffersMarket>;
|
||||
trades$: Observable<Trade[]>;
|
||||
radioGroupForm: FormGroup;
|
||||
defaultInterval = 'day';
|
||||
|
||||
isLoadingGraph = false;
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private route: ActivatedRoute,
|
||||
private bisqApiService: BisqApiService,
|
||||
private formBuilder: FormBuilder,
|
||||
private seoService: SeoService,
|
||||
private router: Router,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.radioGroupForm = this.formBuilder.group({
|
||||
interval: [this.defaultInterval],
|
||||
});
|
||||
|
||||
if (['half_hour', 'hour', 'half_day', 'day', 'week', 'month', 'year', 'auto'].indexOf(this.route.snapshot.fragment) > -1) {
|
||||
this.radioGroupForm.controls.interval.setValue(this.route.snapshot.fragment, { emitEvent: false });
|
||||
}
|
||||
|
||||
this.currency$ = this.bisqApiService.getMarkets$()
|
||||
.pipe(
|
||||
switchMap((markets) => combineLatest([of(markets), this.route.paramMap])),
|
||||
map(([markets, routeParams]) => {
|
||||
const pair = routeParams.get('pair');
|
||||
const pairUpperCase = pair.replace('_', '/').toUpperCase();
|
||||
this.seoService.setTitle(`Bisq market: ${pairUpperCase}`);
|
||||
|
||||
return {
|
||||
pair: pairUpperCase,
|
||||
market: markets[pair],
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
this.trades$ = this.route.paramMap
|
||||
.pipe(
|
||||
map(routeParams => routeParams.get('pair')),
|
||||
switchMap((marketPair) => this.bisqApiService.getMarketTrades$(marketPair)),
|
||||
);
|
||||
|
||||
this.offers$ = this.route.paramMap
|
||||
.pipe(
|
||||
map(routeParams => routeParams.get('pair')),
|
||||
switchMap((marketPair) => this.bisqApiService.getMarketOffers$(marketPair)),
|
||||
map((offers) => offers[Object.keys(offers)[0]])
|
||||
);
|
||||
|
||||
this.hlocData$ = combineLatest([
|
||||
this.route.paramMap,
|
||||
merge(this.radioGroupForm.get('interval').valueChanges, of(this.radioGroupForm.get('interval').value)),
|
||||
])
|
||||
.pipe(
|
||||
switchMap(([routeParams, interval]) => {
|
||||
this.isLoadingGraph = true;
|
||||
const pair = routeParams.get('pair');
|
||||
return this.bisqApiService.getMarketsHloc$(pair, interval);
|
||||
}),
|
||||
map((hlocData) => {
|
||||
this.isLoadingGraph = false;
|
||||
|
||||
hlocData = hlocData.map((h) => {
|
||||
h.time = h.period_start;
|
||||
return h;
|
||||
});
|
||||
|
||||
const hlocVolume = hlocData.map((h) => {
|
||||
return {
|
||||
time: h.time,
|
||||
value: h.volume_right,
|
||||
color: h.close > h.avg ? 'rgba(0, 41, 74, 0.7)' : 'rgba(0, 41, 74, 1)',
|
||||
};
|
||||
});
|
||||
|
||||
// Add whitespace
|
||||
if (hlocData.length > 1) {
|
||||
const newHloc = [];
|
||||
newHloc.push(hlocData[0]);
|
||||
|
||||
const period = this.getUnixTimestampFromInterval(this.radioGroupForm.get('interval').value); // temp
|
||||
let periods = 0;
|
||||
const startingDate = hlocData[0].period_start;
|
||||
let index = 1;
|
||||
while (true) {
|
||||
periods++;
|
||||
if (hlocData[index].period_start > startingDate + period * periods) {
|
||||
newHloc.push({
|
||||
time: startingDate + period * periods,
|
||||
});
|
||||
} else {
|
||||
newHloc.push(hlocData[index]);
|
||||
index++;
|
||||
if (!hlocData[index]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
hlocData = newHloc;
|
||||
}
|
||||
|
||||
return {
|
||||
hloc: hlocData,
|
||||
volume: hlocVolume,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
setFragment(fragment: string) {
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: fragment
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.websocketService.stopTrackingBisqMarket();
|
||||
}
|
||||
|
||||
getUnixTimestampFromInterval(interval: string): number {
|
||||
switch (interval) {
|
||||
case 'minute': return 60;
|
||||
case 'half_hour': return 1800;
|
||||
case 'hour': return 3600;
|
||||
case 'half_day': return 43200;
|
||||
case 'day': return 86400;
|
||||
case 'week': return 604800;
|
||||
case 'month': return 2592000;
|
||||
case 'year': return 31579200;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,20 +5,20 @@
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="col-md">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody *ngIf="!isLoading; else loadingTemplate">
|
||||
<tr>
|
||||
<td class="td-width" i18n="BSQ existing amount">Existing amount</td>
|
||||
<td>{{ (stats.minted - stats.burnt) | number: '1.2-2' }} BSQ</td>
|
||||
<td>{{ (stats.minted - stats.burnt) | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="BSQ minted amount">Minted amount</td>
|
||||
<td>{{ stats.minted | number: '1.2-2' }} BSQ</td>
|
||||
<td>{{ stats.minted | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="BSQ burnt amount">Burnt amount</td>
|
||||
<td>{{ stats.burnt | number: '1.2-2' }} BSQ</td>
|
||||
<td>{{ stats.burnt | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="BSQ addresses">Addresses</td>
|
||||
@@ -33,7 +33,7 @@
|
||||
<td>{{ stats.spent_txos | number }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="BSQ token price">Price</td>
|
||||
<td i18n>Price</td>
|
||||
<td><app-fiat [value]="price"></app-fiat></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -44,7 +44,7 @@
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<div class="col-sm"></div>
|
||||
<div class="col-md"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,3 +7,12 @@
|
||||
width: 175px;
|
||||
}
|
||||
}
|
||||
|
||||
.fiat {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
@media (min-width: 768px) {
|
||||
font-size: 14px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { BisqApiService } from '../bisq-api.service';
|
||||
import { BisqStats } from '../bisq.interfaces';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-stats',
|
||||
@@ -15,12 +16,15 @@ export class BisqStatsComponent implements OnInit {
|
||||
price: number;
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private bisqApiService: BisqApiService,
|
||||
private seoService: SeoService,
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.seoService.setTitle($localize`:@@2a30a4cdb123a03facc5ab8c5b3e6d8b8dbbc3d4:BSQ statistics`);
|
||||
this.stateService.bsqPrice$
|
||||
.subscribe((bsqPrice) => {
|
||||
|
||||
46
frontend/src/app/bisq/bisq-trades/bisq-trades.component.html
Normal file
46
frontend/src/app/bisq/bisq-trades/bisq-trades.component.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<div class="table-container">
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<th i18n>Date</th>
|
||||
<th *ngIf="view === 'all'" i18n>Price</th>
|
||||
<th><ng-container *ngTemplateOutlet="amount; context: {$implicit: 'BTC' }"></ng-container></th>
|
||||
<th>
|
||||
<ng-template [ngIf]="market" [ngIfElse]="noMarket"><ng-container *ngTemplateOutlet="amount; context: {$implicit: market.lsymbol === 'BTC' ? market.rsymbol : market.lsymbol }"></ng-container></ng-template>
|
||||
<ng-template #noMarket i18n>Amount</ng-template>
|
||||
</th>
|
||||
</thead>
|
||||
<tbody *ngIf="(trades$ | async) as trades; else loadingTmpl">
|
||||
<tr *ngFor="let trade of trades;">
|
||||
<td>
|
||||
{{ trade.trade_date | date:'yyyy-MM-dd HH:mm' }}
|
||||
</td>
|
||||
<td *ngIf="view === 'all'">
|
||||
<ng-container *ngIf="(trade._market || market).rtype === 'fiat'; else priceCrypto"><span class="green-color">{{ trade.price | currency: (trade._market || market).rsymbol }}</span></ng-container>
|
||||
<ng-template #priceCrypto>{{ trade.price | number: '1.2-' + (trade._market || market).rprecision }} <span class="symbol">{{ (trade._market || market).rsymbol }}</span></ng-template>
|
||||
</td>
|
||||
<ng-container *ngTemplateOutlet="(trade._market || market).rsymbol === 'BTC' ? tradeVolume : tradeAmount"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="(trade._market || market).rsymbol === 'BTC' ? tradeAmount : tradeVolume"></ng-container>
|
||||
<ng-template #tradeAmount>
|
||||
<td>
|
||||
<ng-container *ngIf="(trade._market || market).ltype === 'fiat'; else amountCrypto"><span class="green-color">{{ trade.amount | currency: (trade._market || market).rsymbol }}</span></ng-container>
|
||||
<ng-template #amountCrypto>{{ trade.amount | number: '1.2-' + (trade._market || market).lprecision }} <span class="symbol">{{ (trade._market || market).lsymbol }}</span></ng-template>
|
||||
</td>
|
||||
</ng-template>
|
||||
<ng-template #tradeVolume>
|
||||
<td>
|
||||
<ng-container *ngIf="(trade._market || market).rtype === 'fiat'; else volumeCrypto"><span class="green-color">{{ trade.volume | currency: (trade._market || market).rsymbol }}</span></ng-container>
|
||||
<ng-template #volumeCrypto>{{ trade.volume | number: '1.2-' + (trade._market || market).rprecision }} <span class="symbol">{{ (trade._market || market).rsymbol }}</span></ng-template>
|
||||
</td>
|
||||
</ng-template>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingTmpl>
|
||||
<tr *ngFor="let i of [1,2,3,4,5,6,7,8,9,10]">
|
||||
<td *ngFor="let j of loadingColumns"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #amount let-i i18n="Trade amount (Symbol)">Amount ({{ i }})</ng-template>
|
||||
12
frontend/src/app/bisq/bisq-trades/bisq-trades.component.scss
Normal file
12
frontend/src/app/bisq/bisq-trades/bisq-trades.component.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
.table-container {
|
||||
overflow: scroll;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
font-size: 13px;
|
||||
@media(min-width: 576px){
|
||||
font-size: 16px;
|
||||
}
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
22
frontend/src/app/bisq/bisq-trades/bisq-trades.component.ts
Normal file
22
frontend/src/app/bisq/bisq-trades/bisq-trades.component.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-trades',
|
||||
templateUrl: './bisq-trades.component.html',
|
||||
styleUrls: ['./bisq-trades.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BisqTradesComponent implements OnChanges {
|
||||
@Input() trades$: Observable<any>;
|
||||
@Input() market: any;
|
||||
@Input() view: 'all' | 'small' = 'all';
|
||||
|
||||
loadingColumns = [1, 2, 3, 4];
|
||||
|
||||
ngOnChanges() {
|
||||
if (this.view === 'small') {
|
||||
this.loadingColumns = [1, 2, 3];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="col-md">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width" i18n="transaction.inputs">Inputs</td>
|
||||
<td>{{ totalInput / 100 | number: '1.2-2' }} BSQ</td>
|
||||
<td>{{ totalInput / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="transaction.outputs">Outputs</td>
|
||||
<td>{{ totalOutput / 100 | number: '1.2-2' }} BSQ</td>
|
||||
<td>{{ totalOutput / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="asset.issued-amount|Liquid Asset issued amount">Issued amount</td>
|
||||
<td>{{ totalIssued / 100 | number: '1.2-2' }} BSQ</td>
|
||||
<td>{{ totalIssued / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="col-md">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody class="mobile-even">
|
||||
<tr>
|
||||
|
||||
@@ -8,4 +8,15 @@
|
||||
.mobile-even tr:nth-of-type(odd) {
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
tr td {
|
||||
&:last-child{
|
||||
text-align: right;
|
||||
@media(min-width: 768px){
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
<div class="container-xl">
|
||||
|
||||
<h1 class="float-left mr-3 mb-md-3" i18n="shared.transaction">Transaction</h1>
|
||||
|
||||
<ng-template [ngIf]="!isLoading && !error">
|
||||
|
||||
<button *ngIf="(latestBlock$ | async) as latestBlock" type="button" class="btn btn-sm btn-success float-right mr-2 mt-1 mt-md-3">
|
||||
<button *ngIf="(latestBlock$ | async) as latestBlock" type="button" class="btn btn-sm btn-success float-right">
|
||||
<ng-container *ngTemplateOutlet="latestBlock.height - bisqTx.blockHeight + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - bisqTx.blockHeight + 1}"></ng-container>
|
||||
<ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
|
||||
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
|
||||
</button>
|
||||
<div>
|
||||
<a [routerLink]="['/bisq-tx' | relativeUrl, bisqTx.id]" style="line-height: 56px;">
|
||||
|
||||
|
||||
<h1 i18n="shared.transaction">Transaction</h1>
|
||||
|
||||
<div class="tx-link">
|
||||
<a [routerLink]="['/bisq-tx' | relativeUrl, bisqTx.id]">
|
||||
<span class="d-inline d-lg-none">{{ bisqTx.id | shortenString : 24 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ bisqTx.id }}</span>
|
||||
</a>
|
||||
@@ -18,7 +20,7 @@
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="box">
|
||||
<div class="box transaction-container">
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
@@ -28,7 +30,7 @@
|
||||
<td>
|
||||
{{ bisqTx.time | date:'yyyy-MM-dd HH:mm' }}
|
||||
<div class="lg-inline">
|
||||
<i>(<app-time-since [time]="bisqTx.time / 1000" [fastRender]="true"></app-time-since>)</i>
|
||||
<i class="symbol">(<app-time-since [time]="bisqTx.time / 1000" [fastRender]="true"></app-time-since>)</i>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -56,12 +58,12 @@
|
||||
<tr>
|
||||
<td class="td-width" i18n="BSQ burnt amount">Burnt amount</td>
|
||||
<td>
|
||||
{{ bisqTx.burntFee / 100 | number: '1.2-2' }} BSQ (<app-bsq-amount [bsq]="bisqTx.burntFee" [forceFiat]="true" [green]="true"></app-bsq-amount>)
|
||||
{{ bisqTx.burntFee / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span> <span class="fiat"><app-bsq-amount [bsq]="bisqTx.burntFee" [forceFiat]="true" [green]="true"></app-bsq-amount></span>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="transaction.fee-per-vbyte|Transaction fee">Fee per vByte</td>
|
||||
<td *ngIf="!isLoadingTx; else loadingTxFee">
|
||||
{{ tx.fee / (tx.weight / 4) | number : '1.1-1' }} sat/vB
|
||||
{{ tx.fee / (tx.weight / 4) | number : '1.1-1' }} <span class="symbol">sat/vB</span>
|
||||
|
||||
<app-tx-fee-rating [tx]="tx"></app-tx-fee-rating>
|
||||
</td>
|
||||
|
||||
@@ -1,9 +1,126 @@
|
||||
.td-width {
|
||||
width: 175px;
|
||||
.adjust-btn-padding {
|
||||
padding: 0.55rem;
|
||||
}
|
||||
|
||||
.title-block {
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.td-width {
|
||||
width: 150px;
|
||||
.mobile-bottomcol {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.td-width {
|
||||
width: 150px;
|
||||
@media (min-width: 768px) {
|
||||
width: 175px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
h1 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.btn-small-height {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.arrow-green {
|
||||
color: #1a9436;
|
||||
}
|
||||
|
||||
.arrow-red {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
float: right;
|
||||
margin-bottom: 0px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.tx-link {
|
||||
display: block;
|
||||
width: auto;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 2px;
|
||||
margin-top: 40px;
|
||||
position: absolute;
|
||||
@media (min-width: 700px) {
|
||||
margin-top: 14px;
|
||||
margin-left: 10px;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
top: 14px;
|
||||
left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.container-xl {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.row{
|
||||
flex-direction: column;
|
||||
@media (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.mobile-bottomcol {
|
||||
margin-top: 15px;
|
||||
}
|
||||
.details-table td:first-child {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.fiat {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
@media (min-width: 576px){
|
||||
display: inline-block;
|
||||
margin-left: 15px;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
h1{
|
||||
font-size: 1.75rem;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 0;
|
||||
float: left;
|
||||
padding-bottom: 40px;
|
||||
@media (min-width: 375px){
|
||||
margin-top: 0px;
|
||||
font-size: 2rem;
|
||||
}
|
||||
@media (min-width: 768px){
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
.transaction-container {
|
||||
font-size: 14px;
|
||||
@media (min-width: 576px){
|
||||
font-size: 16px;
|
||||
}
|
||||
tr td {
|
||||
&:last-child{
|
||||
text-align: right;
|
||||
@media (min-width: 768px){
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { BisqApiService } from '../bisq-api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { ElectrsApiService } from 'src/app/services/electrs-api.service';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-transaction',
|
||||
@@ -27,6 +28,7 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
|
||||
subscription: Subscription;
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private route: ActivatedRoute,
|
||||
private bisqApiService: BisqApiService,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
@@ -36,6 +38,8 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.subscription = this.route.paramMap.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.isLoading = true;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="container-xl">
|
||||
<h1 style="float: left;" i18n>Transactions</h1>
|
||||
<h1 style="float: left;" i18n>BSQ Transactions</h1>
|
||||
|
||||
<div class="d-block float-right">
|
||||
<form [formGroup]="radioGroupForm">
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<th style="width: 20%;" i18n>Transaction</th>
|
||||
<th style="width: 20%;" i18n>TXID</th>
|
||||
<th class="d-none d-md-block" style="width: 100%;" i18n>Type</th>
|
||||
<th style="width: 20%;" i18n>Amount</th>
|
||||
<th style="width: 20%;" i18n>Confirmed</th>
|
||||
@@ -26,15 +26,15 @@
|
||||
<td><a [routerLink]="['/tx/' | relativeUrl, tx.id]" [state]="{ data: tx }">{{ tx.id | slice : 0 : 8 }}</a></td>
|
||||
<td class="d-none d-md-block">
|
||||
<app-bisq-icon class="mr-1" [txType]="tx.txType"></app-bisq-icon>
|
||||
<span class="d-none d-md-inline"> {{ tx.txTypeDisplayString }}</span>
|
||||
<span class="d-none d-md-inline"> {{ getStringByTxType(tx.txType) }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<app-bisq-icon class="d-inline d-md-none mr-1" [txType]="tx.txType"></app-bisq-icon>
|
||||
<ng-template [ngIf]="tx.txType === 'PAY_TRADE_FEE' || tx.txType === 'ASSET_LISTING_FEE'" [ngIfElse]="defaultTxType">
|
||||
{{ tx.burntFee / 100 | number: '1.2-2' }}<span class="d-none d-md-inline"> BSQ</span>
|
||||
{{ tx.burntFee / 100 | number: '1.2-2' }} <span class="d-none d-md-inline symbol">BSQ</span>
|
||||
</ng-template>
|
||||
<ng-template #defaultTxType>
|
||||
{{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }}<span class="d-none d-md-inline"> BSQ</span>
|
||||
{{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }} <span class="d-none d-md-inline symbol">BSQ</span>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td><app-time-since [time]="tx.time / 1000" [fastRender]="true"></app-time-since></td>
|
||||
@@ -44,8 +44,9 @@
|
||||
</table>
|
||||
|
||||
<br>
|
||||
|
||||
<ngb-pagination *ngIf="transactions.value" [size]="paginationSize" [collectionSize]="transactions.value[1]" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true"></ngb-pagination>
|
||||
<div class="pagination">
|
||||
<ngb-pagination *ngIf="transactions.value" [size]="paginationSize" [collectionSize]="transactions.value[1]" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true"></ngb-pagination>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -54,4 +55,4 @@
|
||||
<tr *ngFor="let i of loadingItems">
|
||||
<td *ngFor="let j of [1, 2, 3, 4, 5]"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
@@ -7,3 +7,14 @@ label {
|
||||
right: 0px;
|
||||
left: inherit;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
h1{
|
||||
font-size: 1.5rem;
|
||||
@media(min-width: 375px){
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { SeoService } from 'src/app/services/seo.service';
|
||||
import { FormGroup, FormBuilder } from '@angular/forms';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from 'ngx-bootrap-multiselect';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-transactions',
|
||||
@@ -26,19 +27,19 @@ export class BisqTransactionsComponent implements OnInit {
|
||||
types: string[] = [];
|
||||
|
||||
txTypeOptions: IMultiSelectOption[] = [
|
||||
{ id: 1, name: 'Asset listing fee' },
|
||||
{ id: 2, name: 'Blind vote' },
|
||||
{ id: 3, name: 'Compensation request' },
|
||||
{ id: 4, name: 'Genesis' },
|
||||
{ id: 13, name: 'Irregular' },
|
||||
{ id: 5, name: 'Lockup' },
|
||||
{ id: 6, name: 'Pay trade fee' },
|
||||
{ id: 7, name: 'Proof of burn' },
|
||||
{ id: 8, name: 'Proposal' },
|
||||
{ id: 9, name: 'Reimbursement request' },
|
||||
{ id: 10, name: 'Transfer BSQ' },
|
||||
{ id: 11, name: 'Unlock' },
|
||||
{ id: 12, name: 'Vote reveal' },
|
||||
{ id: 1, name: $localize`Asset listing fee` },
|
||||
{ id: 2, name: $localize`Blind vote` },
|
||||
{ id: 3, name: $localize`Compensation request` },
|
||||
{ id: 4, name: $localize`Genesis` },
|
||||
{ id: 13, name: $localize`Irregular` },
|
||||
{ id: 5, name: $localize`Lockup` },
|
||||
{ id: 6, name: $localize`Pay trade fee` },
|
||||
{ id: 7, name: $localize`Proof of burn` },
|
||||
{ id: 8, name: $localize`Proposal` },
|
||||
{ id: 9, name: $localize`Reimbursement request` },
|
||||
{ id: 10, name: $localize`Transfer BSQ` },
|
||||
{ id: 11, name: $localize`Unlock` },
|
||||
{ id: 12, name: $localize`Vote reveal` },
|
||||
];
|
||||
txTypesDefaultChecked = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
|
||||
|
||||
@@ -59,12 +60,13 @@ export class BisqTransactionsComponent implements OnInit {
|
||||
|
||||
// @ts-ignore
|
||||
paginationSize: 'sm' | 'lg' = 'md';
|
||||
paginationMaxSize = 10;
|
||||
paginationMaxSize = 4;
|
||||
|
||||
txTypes = ['ASSET_LISTING_FEE', 'BLIND_VOTE', 'COMPENSATION_REQUEST', 'GENESIS', 'LOCKUP', 'PAY_TRADE_FEE',
|
||||
'PROOF_OF_BURN', 'PROPOSAL', 'REIMBURSEMENT_REQUEST', 'TRANSFER_BSQ', 'UNLOCK', 'VOTE_REVEAL', 'IRREGULAR'];
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private bisqApiService: BisqApiService,
|
||||
private seoService: SeoService,
|
||||
private formBuilder: FormBuilder,
|
||||
@@ -74,6 +76,7 @@ export class BisqTransactionsComponent implements OnInit {
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks']);
|
||||
this.seoService.setTitle($localize`:@@add4cd82e3e38a3110fe67b3c7df56e9602644ee:Transactions`);
|
||||
|
||||
this.radioGroupForm = this.formBuilder.group({
|
||||
@@ -157,6 +160,11 @@ export class BisqTransactionsComponent implements OnInit {
|
||||
return outputs.reduce((acc: number, output: BisqOutput) => acc + output.bsqAmount, 0);
|
||||
}
|
||||
|
||||
getStringByTxType(type: string) {
|
||||
const id = this.txTypes.indexOf(type) + 1;
|
||||
return this.txTypeOptions.find((type) => id === type.id).name;
|
||||
}
|
||||
|
||||
trackByFn(index: number) {
|
||||
return index;
|
||||
}
|
||||
|
||||
@@ -58,11 +58,11 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="float-left mt-2-5" *ngIf="showConfirmations && tx.burntFee">
|
||||
<ng-container i18n="BSQ burnt amount">Burnt amount</ng-container>: {{ tx.burntFee / 100 | number: '1.2-2' }} BSQ (<app-bsq-amount [bsq]="tx.burntFee" [forceFiat]="true" [green]="true"></app-bsq-amount>)
|
||||
<div class="transaction-fee" *ngIf="showConfirmations && tx.burntFee">
|
||||
<ng-container i18n="BSQ burnt amount">Burnt amount</ng-container>: {{ tx.burntFee / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span> <span class="extra-info"><span class="fiat"><app-bsq-amount [bsq]="tx.burntFee" [forceFiat]="true" [green]="true"></app-bsq-amount></span></span>
|
||||
</div>
|
||||
|
||||
<div class="float-right">
|
||||
<div class="btn-container">
|
||||
<span *ngIf="showConfirmations && latestBlock$ | async as latestBlock">
|
||||
<button type="button" class="btn btn-sm btn-success mt-2">
|
||||
<ng-container *ngTemplateOutlet="latestBlock.height - tx.blockHeight + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - tx.blockHeight + 1}"></ng-container>
|
||||
|
||||
@@ -59,26 +59,81 @@
|
||||
background-color:#6c757d;
|
||||
}
|
||||
|
||||
.scriptmessage {
|
||||
max-width: 280px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.scriptmessage.longer {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.mobile-bottomcol {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.scriptmessage {
|
||||
max-width: 90px !important;
|
||||
.details-table td:first-child {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.scriptmessage.longer {
|
||||
max-width: 280px !important;
|
||||
}
|
||||
|
||||
|
||||
.details-table {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.details-table td {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.details-table td:nth-child(2) {
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.smaller-text {
|
||||
font-size: 12px;
|
||||
@media (min-width: 576px) {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.longer {
|
||||
max-width: 100% !important;
|
||||
width: 200px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.row{
|
||||
flex-direction: column;
|
||||
@media (min-width: 992px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.extra-info {
|
||||
display: inline-table;
|
||||
.fiat {
|
||||
font-size: 14px;
|
||||
display: block;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.transaction-fee {
|
||||
display: block;
|
||||
margin: 0px auto 5px;
|
||||
@media (min-width: 576px) {
|
||||
display: inline-table;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.fiat {
|
||||
margin-left: 10px;
|
||||
font-size: 13px;
|
||||
@media (min-width: 576px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
text-align: right;
|
||||
@media (min-width: 576px) {
|
||||
display: inline-table;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
@@ -80,3 +80,182 @@ interface SpentInfo {
|
||||
inputIndex: number;
|
||||
txId: string;
|
||||
}
|
||||
|
||||
|
||||
export interface BisqTrade {
|
||||
direction: string;
|
||||
price: string;
|
||||
amount: string;
|
||||
volume: string;
|
||||
payment_method: string;
|
||||
trade_id: string;
|
||||
trade_date: number;
|
||||
market?: string;
|
||||
}
|
||||
|
||||
export interface Currencies { [txid: string]: Currency; }
|
||||
|
||||
export interface Currency {
|
||||
code: string;
|
||||
name: string;
|
||||
precision: number;
|
||||
|
||||
_type: string;
|
||||
}
|
||||
|
||||
export interface Depth { [market: string]: Market; }
|
||||
|
||||
interface Market {
|
||||
'buys': string[];
|
||||
'sells': string[];
|
||||
}
|
||||
|
||||
export interface HighLowOpenClose {
|
||||
period_start: number | string;
|
||||
open: string;
|
||||
high: string;
|
||||
low: string;
|
||||
close: string;
|
||||
volume_left: string;
|
||||
volume_right: string;
|
||||
avg: string;
|
||||
}
|
||||
|
||||
export interface Markets { [txid: string]: Pair; }
|
||||
|
||||
interface Pair {
|
||||
pair: string;
|
||||
lname: string;
|
||||
rname: string;
|
||||
lsymbol: string;
|
||||
rsymbol: string;
|
||||
lprecision: number;
|
||||
rprecision: number;
|
||||
ltype: string;
|
||||
rtype: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Offers { [market: string]: OffersMarket; }
|
||||
|
||||
export interface OffersMarket {
|
||||
buys: Offer[] | null;
|
||||
sells: Offer[] | null;
|
||||
}
|
||||
|
||||
export interface OffersData {
|
||||
direction: string;
|
||||
currencyCode: string;
|
||||
minAmount: number;
|
||||
amount: number;
|
||||
price: number;
|
||||
date: number;
|
||||
useMarketBasedPrice: boolean;
|
||||
marketPriceMargin: number;
|
||||
paymentMethod: string;
|
||||
id: string;
|
||||
currencyPair: string;
|
||||
primaryMarketDirection: string;
|
||||
priceDisplayString: string;
|
||||
primaryMarketAmountDisplayString: string;
|
||||
primaryMarketMinAmountDisplayString: string;
|
||||
primaryMarketVolumeDisplayString: string;
|
||||
primaryMarketMinVolumeDisplayString: string;
|
||||
primaryMarketPrice: number;
|
||||
primaryMarketAmount: number;
|
||||
primaryMarketMinAmount: number;
|
||||
primaryMarketVolume: number;
|
||||
primaryMarketMinVolume: number;
|
||||
}
|
||||
|
||||
export interface Offer {
|
||||
offer_id: string;
|
||||
offer_date: number;
|
||||
direction: string;
|
||||
min_amount: string;
|
||||
amount: string;
|
||||
price: string;
|
||||
volume: string;
|
||||
payment_method: string;
|
||||
offer_fee_txid: any;
|
||||
}
|
||||
|
||||
export interface Tickers { [market: string]: Ticker | null; }
|
||||
|
||||
export interface Ticker {
|
||||
last: string;
|
||||
high: string;
|
||||
low: string;
|
||||
volume_left: string;
|
||||
volume_right: string;
|
||||
buy: string | null;
|
||||
sell: string | null;
|
||||
}
|
||||
|
||||
export interface Trade {
|
||||
market?: string;
|
||||
price: string;
|
||||
amount: string;
|
||||
volume: string;
|
||||
payment_method: string;
|
||||
trade_id: string;
|
||||
trade_date: number;
|
||||
_market: Pair;
|
||||
}
|
||||
|
||||
export interface TradesData {
|
||||
currency: string;
|
||||
direction: string;
|
||||
tradePrice: number;
|
||||
tradeAmount: number;
|
||||
tradeDate: number;
|
||||
paymentMethod: string;
|
||||
offerDate: number;
|
||||
useMarketBasedPrice: boolean;
|
||||
marketPriceMargin: number;
|
||||
offerAmount: number;
|
||||
offerMinAmount: number;
|
||||
offerId: string;
|
||||
depositTxId?: string;
|
||||
currencyPair: string;
|
||||
primaryMarketDirection: string;
|
||||
primaryMarketTradePrice: number;
|
||||
primaryMarketTradeAmount: number;
|
||||
primaryMarketTradeVolume: number;
|
||||
|
||||
_market: string;
|
||||
_tradePriceStr: string;
|
||||
_tradeAmountStr: string;
|
||||
_tradeVolumeStr: string;
|
||||
_offerAmountStr: string;
|
||||
_tradePrice: number;
|
||||
_tradeAmount: number;
|
||||
_tradeVolume: number;
|
||||
_offerAmount: number;
|
||||
}
|
||||
|
||||
export interface MarketVolume {
|
||||
period_start: number;
|
||||
num_trades: number;
|
||||
volume: string;
|
||||
}
|
||||
|
||||
export interface MarketsApiError {
|
||||
success: number;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export type Interval = 'minute' | 'half_hour' | 'hour' | 'half_day' | 'day' | 'week' | 'month' | 'year' | 'auto';
|
||||
|
||||
export interface SummarizedIntervals { [market: string]: SummarizedInterval; }
|
||||
export interface SummarizedInterval {
|
||||
period_start: number;
|
||||
open: number;
|
||||
close: number;
|
||||
high: number;
|
||||
low: number;
|
||||
avg: number;
|
||||
volume_right: number;
|
||||
volume_left: number;
|
||||
time?: number;
|
||||
}
|
||||
|
||||
@@ -3,10 +3,15 @@ import { BisqRoutingModule } from './bisq.routing.module';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { NgxBootstrapMultiselectModule } from 'ngx-bootrap-multiselect';
|
||||
|
||||
import { LightweightChartsComponent } from './lightweight-charts/lightweight-charts.component';
|
||||
import { LightweightChartsAreaComponent } from './lightweight-charts-area/lightweight-charts-area.component';
|
||||
import { BisqMarketComponent } from './bisq-market/bisq-market.component';
|
||||
import { BisqTransactionsComponent } from './bisq-transactions/bisq-transactions.component';
|
||||
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.component';
|
||||
import { BisqBlockComponent } from './bisq-block/bisq-block.component';
|
||||
import { BisqDashboardComponent } from './bisq-dashboard/bisq-dashboard.component';
|
||||
import { BisqMainDashboardComponent } from './bisq-main-dashboard/bisq-main-dashboard.component';
|
||||
import { BisqIconComponent } from './bisq-icon/bisq-icon.component';
|
||||
import { BisqTransactionDetailsComponent } from './bisq-transaction-details/bisq-transaction-details.component';
|
||||
import { BisqTransfersComponent } from './bisq-transfers/bisq-transfers.component';
|
||||
@@ -14,11 +19,11 @@ import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontaweso
|
||||
import { faLeaf, faQuestion, faExclamationTriangle, faRocket, faRetweet, faFileAlt, faMoneyBill,
|
||||
faEye, faEyeSlash, faLock, faLockOpen, faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { BisqBlocksComponent } from './bisq-blocks/bisq-blocks.component';
|
||||
import { BisqExplorerComponent } from './bisq-explorer/bisq-explorer.component';
|
||||
import { BisqApiService } from './bisq-api.service';
|
||||
import { BisqAddressComponent } from './bisq-address/bisq-address.component';
|
||||
import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
|
||||
import { BsqAmountComponent } from './bsq-amount/bsq-amount.component';
|
||||
import { BisqTradesComponent } from './bisq-trades/bisq-trades.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -30,10 +35,15 @@ import { BsqAmountComponent } from './bsq-amount/bsq-amount.component';
|
||||
BisqTransactionDetailsComponent,
|
||||
BisqTransfersComponent,
|
||||
BisqBlocksComponent,
|
||||
BisqExplorerComponent,
|
||||
BisqAddressComponent,
|
||||
BisqStatsComponent,
|
||||
BsqAmountComponent,
|
||||
LightweightChartsComponent,
|
||||
LightweightChartsAreaComponent,
|
||||
BisqDashboardComponent,
|
||||
BisqMarketComponent,
|
||||
BisqTradesComponent,
|
||||
BisqMainDashboardComponent,
|
||||
],
|
||||
imports: [
|
||||
BisqRoutingModule,
|
||||
|
||||
@@ -5,55 +5,68 @@ import { BisqTransactionsComponent } from './bisq-transactions/bisq-transactions
|
||||
import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.component';
|
||||
import { BisqBlockComponent } from './bisq-block/bisq-block.component';
|
||||
import { BisqBlocksComponent } from './bisq-blocks/bisq-blocks.component';
|
||||
import { BisqExplorerComponent } from './bisq-explorer/bisq-explorer.component';
|
||||
import { BisqAddressComponent } from './bisq-address/bisq-address.component';
|
||||
import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
|
||||
import { ApiDocsComponent } from '../components/api-docs/api-docs.component';
|
||||
import { BisqDashboardComponent } from './bisq-dashboard/bisq-dashboard.component';
|
||||
import { BisqMarketComponent } from './bisq-market/bisq-market.component';
|
||||
import { BisqMainDashboardComponent } from './bisq-main-dashboard/bisq-main-dashboard.component';
|
||||
import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: BisqExplorerComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: BisqTransactionsComponent
|
||||
},
|
||||
{
|
||||
path: 'tx/:id',
|
||||
component: BisqTransactionComponent
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
children: [],
|
||||
component: BisqBlocksComponent
|
||||
},
|
||||
{
|
||||
path: 'block/:id',
|
||||
component: BisqBlockComponent,
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
component: BisqAddressComponent,
|
||||
},
|
||||
{
|
||||
path: 'stats',
|
||||
component: BisqStatsComponent,
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
component: AboutComponent,
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
component: ApiDocsComponent,
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
{
|
||||
path: '',
|
||||
component: BisqMainDashboardComponent,
|
||||
},
|
||||
{
|
||||
path: 'markets',
|
||||
component: BisqDashboardComponent,
|
||||
},
|
||||
{
|
||||
path: 'transactions',
|
||||
component: BisqTransactionsComponent
|
||||
},
|
||||
{
|
||||
path: 'market/:pair',
|
||||
component: BisqMarketComponent,
|
||||
},
|
||||
{
|
||||
path: 'tx/:id',
|
||||
component: BisqTransactionComponent
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
children: [],
|
||||
component: BisqBlocksComponent
|
||||
},
|
||||
{
|
||||
path: 'block/:id',
|
||||
component: BisqBlockComponent,
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
component: BisqAddressComponent,
|
||||
},
|
||||
{
|
||||
path: 'stats',
|
||||
component: BisqStatsComponent,
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
component: AboutComponent,
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
component: ApiDocsComponent,
|
||||
},
|
||||
{
|
||||
path: 'terms-of-service',
|
||||
component: TermsOfServiceComponent
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
<span [class.green-color]="green">{{ conversions.USD * bsq / 100 * (bsqPrice$ | async) / 100000000 | currency:'USD':'symbol':'1.2-2' }}</span>
|
||||
</ng-container>
|
||||
<ng-template #viewFiatVin>
|
||||
{{ bsq / 100 | number : digitsInfo }} BSQ
|
||||
{{ bsq / 100 | number : digitsInfo }} <span class="symbol">BSQ</span>
|
||||
</ng-template>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
:host ::ng-deep .floating-tooltip-2 {
|
||||
width: 160px;
|
||||
height: 80px;
|
||||
position: absolute;
|
||||
display: none;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
color:rgba(255, 255, 255, 1);
|
||||
background-color: #131722;
|
||||
text-align: left;
|
||||
z-index: 1000;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
pointer-events: none;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
:host ::ng-deep .volumeText {
|
||||
color: rgba(33, 150, 243, 0.7);
|
||||
}
|
||||
|
||||
:host ::ng-deep .tradesText {
|
||||
color: rgba(37, 177, 53, 1);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { createChart, CrosshairMode, isBusinessDay } from 'lightweight-charts';
|
||||
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-lightweight-charts-area',
|
||||
template: '<ng-component></ng-component>',
|
||||
styleUrls: ['./lightweight-charts-area.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class LightweightChartsAreaComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() data: any;
|
||||
@Input() lineData: any;
|
||||
@Input() precision: number;
|
||||
@Input() height = 500;
|
||||
|
||||
areaSeries: any;
|
||||
volumeSeries: any;
|
||||
chart: any;
|
||||
lineSeries: any;
|
||||
container: any;
|
||||
|
||||
width: number;
|
||||
|
||||
constructor(
|
||||
private element: ElementRef,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.width = this.element.nativeElement.parentElement.offsetWidth;
|
||||
this.container = document.createElement('div');
|
||||
const chartholder = this.element.nativeElement.appendChild(this.container);
|
||||
|
||||
this.chart = createChart(chartholder, {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
crosshair: {
|
||||
mode: CrosshairMode.Normal,
|
||||
},
|
||||
layout: {
|
||||
backgroundColor: '#000',
|
||||
textColor: 'rgba(255, 255, 255, 0.8)',
|
||||
},
|
||||
grid: {
|
||||
vertLines: {
|
||||
color: 'rgba(255, 255, 255, 0.1)',
|
||||
},
|
||||
horzLines: {
|
||||
color: 'rgba(255, 255, 255, 0.1)',
|
||||
},
|
||||
},
|
||||
rightPriceScale: {
|
||||
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||
},
|
||||
timeScale: {
|
||||
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||
},
|
||||
});
|
||||
|
||||
this.lineSeries = this.chart.addLineSeries({
|
||||
color: 'rgba(37, 177, 53, 1)',
|
||||
lineColor: 'rgba(216, 27, 96, 1)',
|
||||
lineWidth: 2,
|
||||
});
|
||||
|
||||
this.areaSeries = this.chart.addAreaSeries({
|
||||
topColor: 'rgba(33, 150, 243, 0.7)',
|
||||
bottomColor: 'rgba(33, 150, 243, 0.1)',
|
||||
lineColor: 'rgba(33, 150, 243, 0.1)',
|
||||
lineWidth: 2,
|
||||
});
|
||||
|
||||
const toolTip = document.createElement('div');
|
||||
toolTip.className = 'floating-tooltip-2';
|
||||
chartholder.appendChild(toolTip);
|
||||
|
||||
this.chart.subscribeCrosshairMove((param) => {
|
||||
if (!param.time || param.point.x < 0 || param.point.x > this.width || param.point.y < 0 || param.point.y > this.height) {
|
||||
toolTip.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const dateStr = isBusinessDay(param.time)
|
||||
? this.businessDayToString(param.time)
|
||||
: new Date(param.time * 1000).toLocaleDateString();
|
||||
|
||||
toolTip.style.display = 'block';
|
||||
const price = param.seriesPrices.get(this.areaSeries);
|
||||
const line = param.seriesPrices.get(this.lineSeries);
|
||||
|
||||
const tradesText = $localize`:@@bisq-graph-trades:Trades`;
|
||||
const volumeText = $localize`:@@bisq-graph-volume:Volume`;
|
||||
|
||||
toolTip.innerHTML = `<table>
|
||||
<tr><td class="tradesText">${tradesText}:</td><td class="text-right tradesText">${Math.round(line * 100) / 100}</td></tr>
|
||||
<tr><td class="volumeText">${volumeText}:<td class="text-right volumeText">${Math.round(price * 100) / 100} BTC</td></tr>
|
||||
</table>
|
||||
<div>${dateStr}</div>`;
|
||||
|
||||
const y = param.point.y;
|
||||
|
||||
const toolTipWidth = 100;
|
||||
const toolTipHeight = 80;
|
||||
const toolTipMargin = 15;
|
||||
|
||||
let left = param.point.x + toolTipMargin;
|
||||
if (left > this.width - toolTipWidth) {
|
||||
left = param.point.x - toolTipMargin - toolTipWidth;
|
||||
}
|
||||
|
||||
let top = y + toolTipMargin;
|
||||
if (top > this.height - toolTipHeight) {
|
||||
top = y - toolTipHeight - toolTipMargin;
|
||||
}
|
||||
|
||||
toolTip.style.left = left + 'px';
|
||||
toolTip.style.top = top + 'px';
|
||||
});
|
||||
|
||||
this.updateData();
|
||||
}
|
||||
|
||||
businessDayToString(businessDay) {
|
||||
return businessDay.year + '-' + businessDay.month + '-' + businessDay.day;
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (!changes.data || changes.data.isFirstChange()){
|
||||
return;
|
||||
}
|
||||
this.updateData();
|
||||
}
|
||||
|
||||
updateData() {
|
||||
this.areaSeries.setData(this.data);
|
||||
this.lineSeries.setData(this.lineData);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.chart.remove();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { createChart, CrosshairMode } from 'lightweight-charts';
|
||||
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-lightweight-charts',
|
||||
template: '<ng-component></ng-component>',
|
||||
styleUrls: ['./lightweight-charts.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class LightweightChartsComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() data: any;
|
||||
@Input() volumeData: any;
|
||||
@Input() precision: number;
|
||||
@Input() height = 500;
|
||||
|
||||
lineSeries: any;
|
||||
volumeSeries: any;
|
||||
chart: any;
|
||||
|
||||
constructor(
|
||||
private element: ElementRef,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.chart = createChart(this.element.nativeElement, {
|
||||
width: this.element.nativeElement.parentElement.offsetWidth,
|
||||
height: this.height,
|
||||
layout: {
|
||||
backgroundColor: '#000000',
|
||||
textColor: '#d1d4dc',
|
||||
},
|
||||
crosshair: {
|
||||
mode: CrosshairMode.Normal,
|
||||
},
|
||||
grid: {
|
||||
vertLines: {
|
||||
visible: true,
|
||||
color: 'rgba(42, 46, 57, 0.5)',
|
||||
},
|
||||
horzLines: {
|
||||
color: 'rgba(42, 46, 57, 0.5)',
|
||||
},
|
||||
},
|
||||
});
|
||||
this.lineSeries = this.chart.addCandlestickSeries();
|
||||
|
||||
this.volumeSeries = this.chart.addHistogramSeries({
|
||||
color: '#26a69a',
|
||||
priceFormat: {
|
||||
type: 'volume',
|
||||
},
|
||||
priceScaleId: '',
|
||||
scaleMargins: {
|
||||
top: 0.85,
|
||||
bottom: 0,
|
||||
},
|
||||
});
|
||||
|
||||
this.updateData();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (!changes.data || changes.data.isFirstChange()){
|
||||
return;
|
||||
}
|
||||
this.updateData();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.chart.remove();
|
||||
}
|
||||
|
||||
updateData() {
|
||||
this.lineSeries.setData(this.data);
|
||||
this.volumeSeries.setData(this.volumeData);
|
||||
|
||||
this.lineSeries.applyOptions({
|
||||
priceFormat: {
|
||||
type: 'price',
|
||||
precision: this.precision,
|
||||
minMove: 0.0000001,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,408 +1,214 @@
|
||||
<div class="container-xl">
|
||||
<div class="text-center">
|
||||
<br>
|
||||
<img src="./resources/mempool-logo-bigger.png" height="62.5" width="250">
|
||||
<br>
|
||||
<div class="container-xl about-page">
|
||||
|
||||
<div class="text-small text-center offset-md-1">
|
||||
<div class="intro">
|
||||
<span style="margin-left: auto; margin-right: -20px; margin-bottom: -20px">™</span>
|
||||
<img class="logo" src="./resources/mempool-logo-bigger.png" />
|
||||
<div class="version">
|
||||
v{{ packetJsonVersion }} [{{ frontendGitCommitHash }}]
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<div class="about-text">
|
||||
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container> ™</h5>
|
||||
<p i18n>Building a mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, without any advertising, altcoins, or third-party trackers.</p>
|
||||
</div>
|
||||
|
||||
<h5 i18n="about.about-the-project">The Mempool Open Source Project</h5>
|
||||
<div class="row row-cols-1">
|
||||
<div class="col col-md-6 mx-auto">
|
||||
<p i18n>An explorer and API developed and operated for the Bitcoin community, focusing on the emerging transaction fee market to help our transition into a multi-layer ecosystem, without ads, altcoins, or third-party trackers.</p>
|
||||
</div>
|
||||
<div class="social-icons">
|
||||
<a target="_blank" href="https://github.com/mempool/mempool">
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="github" class="svg-inline--fa fa-github fa-w-16 fa-4x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path fill="currentColor" d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"></path></svg>
|
||||
</a>
|
||||
<a target="_blank" href="https://twitter.com/mempool">
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="twitter" class="svg-inline--fa fa-twitter fa-w-16 fa-4x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"></path></svg>
|
||||
</a>
|
||||
<a target="_blank" href="https://t.me/mempoolspace">
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="telegram-plane" class="svg-inline--fa fa-telegram-plane fa-w-14 fa-4x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M446.7 98.6l-67.6 318.8c-5.1 22.5-18.4 28.1-37.3 17.5l-103-75.9-49.7 47.8c-5.5 5.5-10.1 10.1-20.7 10.1l7.4-104.9 190.9-172.5c8.3-7.4-1.8-11.5-12.9-4.1L117.8 284 16.2 252.2c-22.1-6.9-22.5-22.1 4.6-32.7L418.2 66.4c18.4-6.9 34.5 4.1 28.5 32.2z"></path></svg>
|
||||
</a>
|
||||
<a target="_blank" href="https://keybase.io/team/mempool">
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="keybase" class="svg-inline--fa fa-keybase fa-w-14 fa-4x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M286.17 419a18 18 0 1 0 18 18 18 18 0 0 0-18-18zm111.92-147.6c-9.5-14.62-39.37-52.45-87.26-73.71q-9.1-4.06-18.38-7.27a78.43 78.43 0 0 0-47.88-104.13c-12.41-4.1-23.33-6-32.41-5.77-.6-2-1.89-11 9.4-35L198.66 32l-5.48 7.56c-8.69 12.06-16.92 23.55-24.34 34.89a51 51 0 0 0-8.29-1.25c-41.53-2.45-39-2.33-41.06-2.33-50.61 0-50.75 52.12-50.75 45.88l-2.36 36.68c-1.61 27 19.75 50.21 47.63 51.85l8.93.54a214 214 0 0 0-46.29 35.54C14 304.66 14 374 14 429.77v33.64l23.32-29.8a148.6 148.6 0 0 0 14.56 37.56c5.78 10.13 14.87 9.45 19.64 7.33 4.21-1.87 10-6.92 3.75-20.11a178.29 178.29 0 0 1-15.76-53.13l46.82-59.83-24.66 74.11c58.23-42.4 157.38-61.76 236.25-38.59 34.2 10.05 67.45.69 84.74-23.84.72-1 1.2-2.16 1.85-3.22a156.09 156.09 0 0 1 2.8 28.43c0 23.3-3.69 52.93-14.88 81.64-2.52 6.46 1.76 14.5 8.6 15.74 7.42 1.57 15.33-3.1 18.37-11.15C429 443 434 414 434 382.32c0-38.58-13-77.46-35.91-110.92zM142.37 128.58l-15.7-.93-1.39 21.79 13.13.78a93 93 0 0 0 .32 19.57l-22.38-1.34a12.28 12.28 0 0 1-11.76-12.79L107 119c1-12.17 13.87-11.27 13.26-11.32l29.11 1.73a144.35 144.35 0 0 0-7 19.17zm148.42 172.18a10.51 10.51 0 0 1-14.35-1.39l-9.68-11.49-34.42 27a8.09 8.09 0 0 1-11.13-1.08l-15.78-18.64a7.38 7.38 0 0 1 1.34-10.34l34.57-27.18-14.14-16.74-17.09 13.45a7.75 7.75 0 0 1-10.59-1s-3.72-4.42-3.8-4.53a7.38 7.38 0 0 1 1.37-10.34L214 225.19s-18.51-22-18.6-22.14a9.56 9.56 0 0 1 1.74-13.42 10.38 10.38 0 0 1 14.3 1.37l81.09 96.32a9.58 9.58 0 0 1-1.74 13.44zM187.44 419a18 18 0 1 0 18 18 18 18 0 0 0-18-18z"></path></svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="enterprise-sponsor">
|
||||
<h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3>
|
||||
<div class="wrapper">
|
||||
<a href="https://squarecrypto.org/" target="_blank" title="Square Crypto">
|
||||
<img class="image" src="/resources/profile/sqcrypto.svg" />
|
||||
<span>Square</span>
|
||||
</a>
|
||||
<a href="https://gemini.com/" target="_blank" title="Gemini">
|
||||
<img class="image" src="/resources/profile/gemini.svg" />
|
||||
<span>Gemini</span>
|
||||
</a>
|
||||
<a href="https://exodus.com/" target="_blank" title="Exodus">
|
||||
<img class="image" src="/resources/profile/exodus.svg" />
|
||||
<span>Exodus</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a target="_blank" class="m-2 fw6 mb3 mt2 truncate black-80 f4 link" href="https://github.com/mempool/mempool">
|
||||
<span class="dib v-mid">
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="github" class="svg-inline--fa fa-github fa-w-16 fa-4x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path fill="currentColor" d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"></path></svg>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a target="_blank" class="m-2 fw6 mb3 mt2 truncate black-80 f4 link" href="https://twitter.com/mempool">
|
||||
<span class="dib v-mid">
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="twitter" class="svg-inline--fa fa-twitter fa-w-16 fa-4x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"></path></svg>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a target="_blank" class="m-2 fw6 mb3 mt2 truncate black-80 f4 link" href="https://t.me/mempoolspace">
|
||||
<span class="dib v-mid">
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="telegram-plane" class="svg-inline--fa fa-telegram-plane fa-w-14 fa-4x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M446.7 98.6l-67.6 318.8c-5.1 22.5-18.4 28.1-37.3 17.5l-103-75.9-49.7 47.8c-5.5 5.5-10.1 10.1-20.7 10.1l7.4-104.9 190.9-172.5c8.3-7.4-1.8-11.5-12.9-4.1L117.8 284 16.2 252.2c-22.1-6.9-22.5-22.1 4.6-32.7L418.2 66.4c18.4-6.9 34.5 4.1 28.5 32.2z"></path></svg>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a target="_blank" class="m-2 fw6 mb3 mt2 truncate black-80 f4 link" href="https://keybase.io/team/mempool">
|
||||
<span class="dib v-mid">
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="keybase" class="svg-inline--fa fa-keybase fa-w-14 fa-4x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M286.17 419a18 18 0 1 0 18 18 18 18 0 0 0-18-18zm111.92-147.6c-9.5-14.62-39.37-52.45-87.26-73.71q-9.1-4.06-18.38-7.27a78.43 78.43 0 0 0-47.88-104.13c-12.41-4.1-23.33-6-32.41-5.77-.6-2-1.89-11 9.4-35L198.66 32l-5.48 7.56c-8.69 12.06-16.92 23.55-24.34 34.89a51 51 0 0 0-8.29-1.25c-41.53-2.45-39-2.33-41.06-2.33-50.61 0-50.75 52.12-50.75 45.88l-2.36 36.68c-1.61 27 19.75 50.21 47.63 51.85l8.93.54a214 214 0 0 0-46.29 35.54C14 304.66 14 374 14 429.77v33.64l23.32-29.8a148.6 148.6 0 0 0 14.56 37.56c5.78 10.13 14.87 9.45 19.64 7.33 4.21-1.87 10-6.92 3.75-20.11a178.29 178.29 0 0 1-15.76-53.13l46.82-59.83-24.66 74.11c58.23-42.4 157.38-61.76 236.25-38.59 34.2 10.05 67.45.69 84.74-23.84.72-1 1.2-2.16 1.85-3.22a156.09 156.09 0 0 1 2.8 28.43c0 23.3-3.69 52.93-14.88 81.64-2.52 6.46 1.76 14.5 8.6 15.74 7.42 1.57 15.33-3.1 18.37-11.15C429 443 434 414 434 382.32c0-38.58-13-77.46-35.91-110.92zM142.37 128.58l-15.7-.93-1.39 21.79 13.13.78a93 93 0 0 0 .32 19.57l-22.38-1.34a12.28 12.28 0 0 1-11.76-12.79L107 119c1-12.17 13.87-11.27 13.26-11.32l29.11 1.73a144.35 144.35 0 0 0-7 19.17zm148.42 172.18a10.51 10.51 0 0 1-14.35-1.39l-9.68-11.49-34.42 27a8.09 8.09 0 0 1-11.13-1.08l-15.78-18.64a7.38 7.38 0 0 1 1.34-10.34l34.57-27.18-14.14-16.74-17.09 13.45a7.75 7.75 0 0 1-10.59-1s-3.72-4.42-3.8-4.53a7.38 7.38 0 0 1 1.37-10.34L214 225.19s-18.51-22-18.6-22.14a9.56 9.56 0 0 1 1.74-13.42 10.38 10.38 0 0 1 14.3 1.37l81.09 96.32a9.58 9.58 0 0 1-1.74 13.44zM187.44 419a18 18 0 1 0 18 18 18 18 0 0 0-18-18z"></path></svg>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<br><br>
|
||||
<br><br>
|
||||
|
||||
<h3 i18n="about.sponsors.enterprise">Enterprise Sponsors 🚀</h3>
|
||||
|
||||
<a href="https://squarecrypto.org/" target="_blank">
|
||||
<div class="profile_photo enterprise_sponsor d-inline-block" title="Square Crypto">
|
||||
<img class="profile_img" src="/resources/profile/sqcrypto.svg" />
|
||||
Square
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="https://gemini.com/" target="_blank">
|
||||
<div class="profile_photo enterprise_sponsor d-inline-block" title="Gemini">
|
||||
<img class="profile_img" src="/resources/profile/gemini.svg" />
|
||||
Gemini
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="https://exodus.com/" target="_blank">
|
||||
<div class="profile_photo enterprise_sponsor d-inline-block" title="Exodus">
|
||||
<img class="profile_img" src="/resources/profile/exodus.svg" />
|
||||
Exodus
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<br><br>
|
||||
<br><br>
|
||||
|
||||
<div class="community-sponsor">
|
||||
<h3 i18n="about.sponsors.withHeart">Community Sponsors ❤️</h3>
|
||||
|
||||
<div *ngIf="sponsors === null">
|
||||
<br>
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
<ng-template ngFor let-sponsor [ngForOf]="sponsors">
|
||||
<a [href]="'https://twitter.com/' + sponsor.handle" target="_blank" rel="sponsored">
|
||||
<div class="profile_photo community_sponsor d-inline-block" [title]="sponsor.handle">
|
||||
<img class="profile_img" [src]="'/api/v1/donations/images/' + sponsor.handle" />
|
||||
</div>
|
||||
</a>
|
||||
</ng-template>
|
||||
<br><br>
|
||||
|
||||
<button type="button" class="btn btn-primary" (click)="donationStatus = 2" [hidden]="donationStatus !== 1" i18n="about.become-a-sponsor">Become a sponsor ❤️</button>
|
||||
<p *ngIf="donationStatus === 2 && !sponsorsEnabled">
|
||||
<ng-container i18n="about.navigate-to-sponsor">Navigate to <a href="https://mempool.space/about" target="_blank">https://mempool.space/about</a> to sponsor</ng-container>
|
||||
</p>
|
||||
|
||||
<div style="max-width: 300px;" class="mx-auto" [hidden]="donationStatus !== 2 || !sponsorsEnabled">
|
||||
<form [formGroup]="donationForm" (submit)="submitDonation()" class="form">
|
||||
<div class="input-group mb-2">
|
||||
<div class="input-group-prepend" style="width: 42px;">
|
||||
<span class="input-group-text">₿</span>
|
||||
</div>
|
||||
<input formControlName="amount" class="form-control" type="number" min="0.001" step="1E-03">
|
||||
</div>
|
||||
<div class="input-group" *ngIf="donationForm.get('amount').value >= 0.01; else lowAmount">
|
||||
<div class="input-group-prepend" style="width: 42px;">
|
||||
<span class="input-group-text">@</span>
|
||||
</div>
|
||||
<input formControlName="handle" class="form-control" type="text" placeholder="Twitter handle (Optional)">
|
||||
</div>
|
||||
<div class="required" *ngIf="donationForm.get('amount').hasError('required')" i18n="about.sponsor.amount-required">Amount required</div>
|
||||
<div class="required" *ngIf="donationForm.get('amount').hasError('min')" i18n="about.sponsor.minimum-amount">Minimum amount is 0.001 BTC</div>
|
||||
<div class="input-group mt-4">
|
||||
<button class="btn btn-primary mx-auto" type="submit" [disabled]="donationForm.invalid" i18n="about.sponsor.request-invoice">Request invoice</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<ng-template #lowAmount>
|
||||
<div class="input-group mb-4 text-small">
|
||||
<span i18n="about.sponsor.description">If you donate 0.01 BTC or more, your profile photo will be added to the list of sponsors above :)</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<div *ngIf="donationStatus === 3" class="text-center">
|
||||
|
||||
<form [formGroup]="paymentForm">
|
||||
<div class="btn-group btn-group-toggle mb-2" ngbRadioGroup formControlName="method">
|
||||
<label ngbButtonLabel class="btn-primary">
|
||||
<input ngbButton type="radio" value="chain"> <fa-icon [icon]="['fas', 'link']" [fixedWidth]="true" title="Onchain"></fa-icon>
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary" *ngIf="donationObj.addresses.BTC_LightningLike">
|
||||
<input ngbButton type="radio" value="lightning"> <fa-icon [icon]="['fas', 'bolt']" [fixedWidth]="true" title="Lightning"></fa-icon>
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary" *ngIf="donationObj.addresses.LBTC">
|
||||
<input ngbButton type="radio" value="lbtc"> <fa-icon [icon]="['fas', 'tint']" [fixedWidth]="true" title="Liquid Bitcoin"></fa-icon>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ng-template [ngIf]="paymentForm.get('method').value === 'chain'">
|
||||
<div class="qr-wrapper mt-2 mb-2">
|
||||
<a [href]="bypassSecurityTrustUrl('bitcoin:' + donationObj.addresses.BTC + '?amount=' + donationObj.amount)" target="_blank">
|
||||
<app-qrcode imageUrl="./resources/bitcoin-logo.png" [size]="200" [data]="'bitcoin:' + donationObj.addresses.BTC + '?amount=' + donationObj.amount"></app-qrcode>
|
||||
<div class="wrapper">
|
||||
<ng-container *ngIf="sponsors$ | async as sponsors; else loadingSponsors">
|
||||
<ng-template ngFor let-sponsor [ngForOf]="sponsors">
|
||||
<a [href]="'https://twitter.com/' + sponsor.handle" target="_blank" rel="sponsored" [title]="sponsor.handle">
|
||||
<img class="image" [src]="'/api/v1/donations/images/' + sponsor.handle" />
|
||||
</a>
|
||||
</div>
|
||||
<br>
|
||||
<div class="input-group input-group-sm mb-3 mt-3 info-group mx-auto">
|
||||
<input type="text" class="form-control" readonly [value]="donationObj.addresses.BTC">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="button" ><app-clipboard [text]="donationObj.addresses.BTC"></app-clipboard></button>
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size: 12px;">{{ donationObj.amount }} BTC</p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="paymentForm.get('method').value === 'lightning'">
|
||||
<div class="qr-wrapper mt-2 mb-2">
|
||||
<a [href]="bypassSecurityTrustUrl('lightning:' + donationObj.addresses.BTC_LightningLike)" target="_blank">
|
||||
<app-qrcode imageUrl="./resources/bitcoin-logo.png" [size]="200" [data]="donationObj.addresses.BTC_LightningLike.toUpperCase()"></app-qrcode>
|
||||
</a>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<div class="input-group input-group-sm mb-3 mt-3 info-group mx-auto">
|
||||
<input type="text" class="form-control" readonly [value]="donationObj.addresses.BTC_LightningLike">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="button"><app-clipboard [text]="donationObj.addresses.BTC_LightningLike"></app-clipboard></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-sm mb-3 mt-3 info-group mx-auto">
|
||||
<input type="text" class="form-control" readonly value="036f7fad4938521ddc6fc87ab7d6c6a091cef23cad87564a1f55adb806c017575e@103.99.170.198:9735">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="button"><app-clipboard [text]="'036f7fad4938521ddc6fc87ab7d6c6a091cef23cad87564a1f55adb806c017575e@103.99.170.198:9735'"></app-clipboard></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p style="font-size: 10px;"></p>
|
||||
<p style="font-size: 12px;">{{ donationObj.amount }} BTC</p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="paymentForm.get('method').value === 'lbtc'">
|
||||
<div class="qr-wrapper mt-2 mb-2">
|
||||
<a [href]="bypassSecurityTrustUrl('liquidnetwork:' + donationObj.addresses.LBTC + '?amount=' + donationObj.amount + '&assetid=6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d')" target="_blank">
|
||||
<app-qrcode imageUrl="./resources/liquid-bitcoin.png" [size]="200" [data]="'liquidnetwork:' + donationObj.addresses.LBTC + '?amount=' + donationObj.amount + '&assetid=6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d'"></app-qrcode>
|
||||
</a>
|
||||
</div>
|
||||
<br>
|
||||
<div class="input-group input-group-sm mb-3 mt-3 info-group mx-auto">
|
||||
<input type="text" class="form-control" readonly [value]="donationObj.addresses.LBTC">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="button" ><app-clipboard [text]="donationObj.addresses.LBTC"></app-clipboard></button>
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size: 12px;">{{ donationObj.amount }} BTC</p>
|
||||
</ng-template>
|
||||
|
||||
<p i18n="about.sponsor.waiting-for-transaction">Waiting for transaction... </p>
|
||||
<div class="spinner-border text-light"></div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div *ngIf="donationStatus === 4" class="text-center">
|
||||
<h2><span i18n="about.sponsor.donation-confirmed">Donation confirmed!</span><br><span i18n="about.sponsor.thank-you">Thank you!</span></h2>
|
||||
</div>
|
||||
|
||||
<br><br>
|
||||
<br><br>
|
||||
<button [hidden]="showNavigateToSponsor" type="button" class="btn btn-primary" (click)="sponsor()" i18n="about.become-a-sponsor">Become a sponsor ❤️</button>
|
||||
<ng-container *ngIf="showNavigateToSponsor" i18n="about.navigate-to-sponsor">Navigate to <a href="https://mempool.space/sponsor" target="_blank">https://mempool.space/sponsor</a> to sponsor</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="community-integrations-sponsor">
|
||||
<h3 i18n="about.integrations">Community Integrations</h3>
|
||||
|
||||
<a href="https://github.com/bisq-network/bisq" target="_blank">
|
||||
<div class="profile_photo d-inline-block" title="Bisq">
|
||||
<img class="profile_img" src="/resources/profile/bisq_network.png" />
|
||||
Bisq
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/getumbrel/umbrel" target="_blank">
|
||||
<div class="profile_photo d-inline-block" title="Umbrel">
|
||||
<img class="profile_img" src="/resources/profile/umbrel.png" />
|
||||
Umbrel
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/rootzoll/raspiblitz" target="_blank">
|
||||
<div class="profile_photo d-inline-block" title="RaspiBlitz">
|
||||
<img class="profile_img" src="/resources/profile/raspiblitz.jpg" />
|
||||
RaspiBlitz
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/mynodebtc/mynode" target="_blank">
|
||||
<div class="profile_photo d-inline-block" title="MyNode">
|
||||
<img class="profile_img" src="/resources/profile/mynodebtc.jpg" />
|
||||
MyNode
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/RoninDojo/RoninDojo" target="_blank">
|
||||
<div class="profile_photo d-inline-block" title="RoninDojo">
|
||||
<img class="profile_img" src="/resources/profile/ronindojo.png" />
|
||||
RoninDojo
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/spesmilo/electrum" target="_blank">
|
||||
<div class="profile_photo d-inline-block" title="Electrum Wallet">
|
||||
<img class="profile_img" src="/resources/profile/electrum.jpg" />
|
||||
Electrum
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/cryptoadvance/specter-desktop" target="_blank">
|
||||
<div class="profile_photo d-inline-block" title="Specter Wallet">
|
||||
<img class="profile_img" src="/resources/profile/specter.png" />
|
||||
Specter
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/sparrowwallet/sparrow" target="_blank">
|
||||
<div class="profile_photo d-inline-block" title="Sparrow Wallet">
|
||||
<img class="profile_img" src="/resources/profile/sparrow.png" />
|
||||
Sparrow
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/ACINQ/phoenix" target="_blank">
|
||||
<div class="profile_photo d-inline-block" title="Phoenix Wallet by ACINQ">
|
||||
<img class="profile_img" src="/resources/profile/phoenix.jpg" />
|
||||
Phoenix
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/muun/apollo" target="_blank">
|
||||
<div class="profile_photo d-inline-block" title="Muun Wallet">
|
||||
<img class="profile_img" src="/resources/profile/muun.png" />
|
||||
Muun
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/BlueWallet/BlueWallet" target="_blank">
|
||||
<div class="profile_photo d-inline-block" title="BlueWallet">
|
||||
<img class="profile_img" src="/resources/profile/bluewallet.png" />
|
||||
BlueWallet
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/hsjoberg/blixt-wallet" target="_blank">
|
||||
<div class="profile_photo d-inline-block" title="Blixt Wallet">
|
||||
<img class="profile_img" src="/resources/profile/blixt.png" />
|
||||
Blixt
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/Satpile/satpile" target="_blank">
|
||||
<div class="profile_photo d-inline-block" title="Satpile Watch-Only Wallet">
|
||||
<img class="profile_img" src="/resources/profile/satpile.jpg" />
|
||||
Satpile
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/btcontract/lnwallet" target="_blank">
|
||||
<div class="profile_photo d-inline-block" title="Bitcoin Lightning Wallet">
|
||||
<img class="profile_img" src="/resources/profile/blw.png" />
|
||||
BLW
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/pxsocs/warden" target="_blank">
|
||||
<div class="profile_photo d-inline-block" title="WARden Portfolio">
|
||||
<img class="profile_img" src="/resources/profile/warden.jpg" />
|
||||
WARden
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<br><br>
|
||||
<br><br>
|
||||
|
||||
<h3 i18n="about.alliances">Community Alliances</h3>
|
||||
|
||||
<a href="https://liquid.net/">
|
||||
<img src="/resources/profile/liquid.svg" height="52">
|
||||
</a>
|
||||
|
||||
<a href="https://opencrypto.org/">
|
||||
<img src="/resources/profile/copa.png" height="62" style="margin-left: 30px; margin-right: 30px; margin-top: 20px;">
|
||||
</a>
|
||||
|
||||
<a href="https://bisq.network/">
|
||||
<img src="/resources/profile/bisq.svg" height="62">
|
||||
</a>
|
||||
|
||||
<br><br>
|
||||
<br><br>
|
||||
|
||||
<h3 i18n="about.contributors">Project Contributors</h3>
|
||||
|
||||
<div *ngIf="contributors === null">
|
||||
<br>
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
<ng-template ngFor let-contributor [ngForOf]="contributors">
|
||||
<a [href]="'https://github.com/' + contributor.name" target="_blank">
|
||||
<div class="profile_photo project_contributor d-inline-block" [title]="contributor.name">
|
||||
<img class="profile_img" [src]="'/api/v1/contributors/images/' + contributor.id" />
|
||||
{{ contributor.name }}
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<a href="https://github.com/bisq-network/bisq" target="_blank" title="Bisq">
|
||||
<img class="image" src="/resources/profile/bisq_network.png" />
|
||||
<span>Bisq</span>
|
||||
</a>
|
||||
<a href="https://github.com/getumbrel/umbrel" target="_blank" title="Umbrel">
|
||||
<img class="image" src="/resources/profile/umbrel.png" />
|
||||
<span>Umbrel</span>
|
||||
</a>
|
||||
<a href="https://github.com/rootzoll/raspiblitz" target="_blank" title="RaspiBlitz">
|
||||
<img class="image" src="/resources/profile/raspiblitz.jpg" />
|
||||
<span>RaspiBlitz</span>
|
||||
</a>
|
||||
<a href="https://github.com/mynodebtc/mynode" target="_blank" title="MyNode">
|
||||
<img class="image" src="/resources/profile/mynodebtc.jpg" />
|
||||
<span>MyNode</span>
|
||||
</a>
|
||||
<a href="https://github.com/RoninDojo/RoninDojo" target="_blank" title="RoninDojo">
|
||||
<img class="image" src="/resources/profile/ronindojo.png" />
|
||||
<span>RoninDojo</span>
|
||||
</a>
|
||||
<a href="https://github.com/spesmilo/electrum" target="_blank" title="Electrum Wallet">
|
||||
<img class="image" src="/resources/profile/electrum.jpg" />
|
||||
<span>Electrum</span>
|
||||
</a>
|
||||
<a href="https://github.com/cryptoadvance/specter-desktop" target="_blank" title="Specter Wallet">
|
||||
<img class="image" src="/resources/profile/specter.png" />
|
||||
<span>Specter</span>
|
||||
</a>
|
||||
<a href="https://github.com/sparrowwallet/sparrow" target="_blank" title="Sparrow Wallet">
|
||||
<img class="image" src="/resources/profile/sparrow.png" />
|
||||
<span>Sparrow</span>
|
||||
</a>
|
||||
<a href="https://github.com/ACINQ/phoenix" target="_blank" title="Phoenix Wallet by ACINQ">
|
||||
<img class="image" src="/resources/profile/phoenix.jpg" />
|
||||
<span>Phoenix</span>
|
||||
</a>
|
||||
<a href="https://github.com/muun/apollo" target="_blank" title="Muun Wallet">
|
||||
<img class="image" src="/resources/profile/muun.png" />
|
||||
<span>Muun</span>
|
||||
</a>
|
||||
<a href="https://github.com/BlueWallet/BlueWallet" target="_blank" title="BlueWallet">
|
||||
<img class="image" src="/resources/profile/bluewallet.png" />
|
||||
<span>BlueWallet</span>
|
||||
</a>
|
||||
<a href="https://github.com/hsjoberg/blixt-wallet" target="_blank" title="Blixt Wallet">
|
||||
<img class="image" src="/resources/profile/blixt.png" />
|
||||
<span>Blixt</span>
|
||||
</a>
|
||||
<a href="https://github.com/Satpile/satpile" target="_blank" title="Satpile Watch-Only Wallet">
|
||||
<img class="image" src="/resources/profile/satpile.jpg" />
|
||||
<span>Satpile</span>
|
||||
</a>
|
||||
<a href="https://github.com/btcontract/lnwallet" target="_blank" title="Bitcoin Lightning Wallet">
|
||||
<img class="image" src="/resources/profile/blw.png" />
|
||||
<span>BLW</span>
|
||||
</a>
|
||||
<a href="https://github.com/pxsocs/warden" target="_blank" title="WARden Portfolio">
|
||||
<img class="image" src="/resources/profile/warden.jpg" />
|
||||
<span>WARden</span>
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
<br><br>
|
||||
<br><br>
|
||||
|
||||
<h3 i18n="about.maintainers">Project Maintainers</h3>
|
||||
|
||||
<a href="https://twitter.com/softsimon_" target="_blank">
|
||||
<div class="profile_photo d-inline-block" title="softsimon">
|
||||
<img class="profile_img" src="/resources/profile/softsimon.jpg" />
|
||||
softsimon
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="https://twitter.com/wiz" target="_blank">
|
||||
<div class="profile_photo d-inline-block" title="wiz">
|
||||
<img class="profile_img" src="/resources/profile/wiz.png" />
|
||||
wiz
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<br><br>
|
||||
|
||||
<br>
|
||||
<div class="col-md-8 offset-md-2 text-center">
|
||||
<code>Copyright (c) 2019-2021</code><br>
|
||||
<code>The Mempool Open Source Project</code><br>
|
||||
</div>
|
||||
<br>
|
||||
<div class="col-md-8 offset-md-2 text-left" style="padding-left: 20px">
|
||||
<code>This program is free software; you can redistribute it and/or modify it under the terms of (at your option) either:</code><br>
|
||||
</div>
|
||||
<br>
|
||||
<div class="col-md-8 offset-md-2 text-left" style="padding-left: 40px">
|
||||
<code> 1) the <a href="https://www.gnu.org/licenses/agpl-3.0-standalone.html">GNU Affero General Public License</a> as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>; or</code><br>
|
||||
<br>
|
||||
<code> 2) the <a href="https://www.gnu.org/licenses/gpl-3.0-standalone.html">GNU General Public License</a> as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>.</code><br>
|
||||
</div>
|
||||
<br>
|
||||
<div class="col-md-8 offset-md-2 text-left" style="padding-left: 20px">
|
||||
<code>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the full license terms for more details.</code><br>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<div class="alliances">
|
||||
<h3 i18n="about.alliances">Community Alliances</h3>
|
||||
<div class="wrapper">
|
||||
<a href="https://liquid.net/" title="Liquid Network">
|
||||
<img class="liquid" src="/resources/profile/liquid.svg" />
|
||||
</a>
|
||||
<a href="https://opencrypto.org/" title="Coppa - Crypto Open Patent Alliance">
|
||||
<img class="copa" src="/resources/profile/copa.png" />
|
||||
</a>
|
||||
<a href="https://bisq.network/" title="Bisq Network">
|
||||
<img class="bisq" src="/resources/profile/bisq.svg" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="contributors">
|
||||
<h3 i18n="about.contributors">Project Contributors</h3>
|
||||
<div class="wrapper">
|
||||
<ng-container *ngIf="contributors$ | async as contributors; else loadingSponsors">
|
||||
<ng-template ngFor let-contributor [ngForOf]="contributors">
|
||||
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name">
|
||||
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" />
|
||||
<span>{{ contributor.name }}</span>
|
||||
</a>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="maintainers">
|
||||
<h3 i18n="about.maintainers">Project Maintainers</h3>
|
||||
<div class="wrapper">
|
||||
<a href="https://twitter.com/softsimon_" target="_blank" title="softsimon">
|
||||
<img class="image" src="/resources/profile/softsimon.jpg" />
|
||||
<span>softsimon</span>
|
||||
</a>
|
||||
<a href="https://twitter.com/wiz" target="_blank" title="wiz">
|
||||
<img class="image" src="/resources/profile/wiz.png" />
|
||||
<span>wiz</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="copyright">
|
||||
<div class="title">
|
||||
Copyright (c) 2019-2021<br />
|
||||
The Mempool Open Source Project
|
||||
</div>
|
||||
<p>
|
||||
This program is free software; you can redistribute it and/or modify it under the terms of (at your option) either:<br>
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
1) the <a href="https://www.gnu.org/licenses/agpl-3.0-standalone.html">GNU Affero General Public License</a> as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>; or<br>
|
||||
</li>
|
||||
<li>
|
||||
2) the <a href="https://www.gnu.org/licenses/gpl-3.0-standalone.html">GNU General Public License</a> as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>.<br>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the full license terms for more details.<br>
|
||||
</p>
|
||||
<p>
|
||||
mempool.space™, The Mempool Open Source Project™, and the Mempool block logo are trademarks of Mempool Space K.K. in Japan.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer-links">
|
||||
<a href="/3rdpartylicenses.txt">Third-party Licenses</a>
|
||||
<br>
|
||||
<br>
|
||||
<a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="text-small text-center" *ngIf="officialMempoolSpace">
|
||||
|
||||
<div class="footer-version" *ngIf="officialMempoolSpace">
|
||||
{{ (backendInfo$ | async)?.hostname }} (v{{ (backendInfo$ | async )?.version }}) [{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}]
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<ng-template #loadingSponsors>
|
||||
<br>
|
||||
<div class="spinner-border text-light"></div>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,50 +1,179 @@
|
||||
.qr-wrapper {
|
||||
background-color: #FFF;
|
||||
padding: 10px;
|
||||
display: inline-block;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.profile_photo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-size: 100%, 100%;
|
||||
border-radius: 50%;
|
||||
margin: 25px;
|
||||
line-height: 32px;
|
||||
}
|
||||
.about-page {
|
||||
text-align: center;
|
||||
|
||||
.profile_img {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
border: 0;
|
||||
}
|
||||
.image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-size: 100%, 100%;
|
||||
border-radius: 50%;
|
||||
margin: 25px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.community_sponsor {
|
||||
margin: 6px;
|
||||
}
|
||||
.intro {
|
||||
margin: 25px auto 30px;
|
||||
width: 250px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.logo {
|
||||
height: 62.5px;
|
||||
width: 250px;
|
||||
margin: auto;
|
||||
}
|
||||
.version {
|
||||
text-align: right;
|
||||
font-size: 10px;
|
||||
margin-top: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
.enterprise_sponsor {
|
||||
margin-left: 40px;
|
||||
margin-right: 40px;
|
||||
}
|
||||
.about-text {
|
||||
max-width: 550px;
|
||||
margin: auto;
|
||||
padding: 10px 15px 15px;
|
||||
}
|
||||
|
||||
.project_contributor {
|
||||
width: 130px;
|
||||
margin: 25px auto;
|
||||
}
|
||||
.social-icons {
|
||||
a {
|
||||
margin: auto 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.text-small {
|
||||
font-size: 12px;
|
||||
}
|
||||
.alliances,
|
||||
.enterprise-sponsor,
|
||||
.community-integrations-sponsor,
|
||||
.maintainers {
|
||||
margin-top: 68px;
|
||||
margin-bottom: 68px;
|
||||
}
|
||||
|
||||
.info-group {
|
||||
max-width: 400px;
|
||||
}
|
||||
.maintainers {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.community-sponsor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.wrapper {
|
||||
margin: 20px auto;
|
||||
}
|
||||
.btn-primary {
|
||||
max-width: 250px;
|
||||
margin: auto;
|
||||
height: 45px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #FF0000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.alliances {
|
||||
margin-bottom: 100px;
|
||||
a {
|
||||
&:nth-child(3) {
|
||||
position: relative;
|
||||
top: 10px;
|
||||
}
|
||||
}
|
||||
img {
|
||||
display: inline-block;
|
||||
margin: 15px auto;
|
||||
height: 62px;
|
||||
@media (min-width: 425px) {
|
||||
margin: 15px 60px;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
margin: 15px 120px;
|
||||
}
|
||||
@media (min-width: 850px) {
|
||||
margin: 50px 30px 0px;
|
||||
}
|
||||
}
|
||||
.liquid {
|
||||
top: 7px;
|
||||
position: relative;
|
||||
}
|
||||
.copa {
|
||||
height: auto;
|
||||
top: 23px;
|
||||
position: relative;
|
||||
width: 300px;
|
||||
}
|
||||
.bisq {
|
||||
top: 3px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.enterprise-sponsor,
|
||||
.contributors,
|
||||
.community-sponsor,
|
||||
.community-integrations-sponsor,
|
||||
.maintainers {
|
||||
.wrapper {
|
||||
display: inline-block;
|
||||
a {
|
||||
display: inline-block;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
img {
|
||||
box-shadow: 0px 0px 20px #1bd8f4;
|
||||
}
|
||||
}
|
||||
img, span{
|
||||
display: block;
|
||||
transition: 150ms all;
|
||||
}
|
||||
img {
|
||||
margin: 40px 29px 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.community-sponsor .wrapper {
|
||||
margin: 10px auto 20px;
|
||||
a img {
|
||||
margin: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.copyright {
|
||||
text-align: left;
|
||||
max-width: 620px;
|
||||
padding: 0px 15px;
|
||||
margin: auto;
|
||||
line-height: 1.8;
|
||||
font-size: 87.5%;
|
||||
color: #e83e8c;
|
||||
word-wrap: break-word;
|
||||
font-family: SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;
|
||||
ul {
|
||||
list-style: none;
|
||||
padding-left: 30px;
|
||||
li {
|
||||
margin-bottom: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
text-align: center;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
a {
|
||||
display: inline-block;
|
||||
margin: 15px auto 0px;
|
||||
&:last-child {
|
||||
margin: 20px auto 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-version {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,33 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
||||
import { delay, retryWhen, switchMap, tap } from 'rxjs/operators';
|
||||
import { IBackendInfo } from 'src/app/interfaces/websocket.interface';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about',
|
||||
templateUrl: './about.component.html',
|
||||
styleUrls: ['./about.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AboutComponent implements OnInit, OnDestroy {
|
||||
export class AboutComponent implements OnInit {
|
||||
backendInfo$: Observable<IBackendInfo>;
|
||||
donationForm: FormGroup;
|
||||
paymentForm: FormGroup;
|
||||
donationStatus = 1;
|
||||
sponsors$: Observable<any>;
|
||||
contributors$: Observable<any>;
|
||||
donationObj: any;
|
||||
sponsorsEnabled = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
||||
frontendGitCommitHash = this.stateService.env.GIT_COMMIT_HASH.substr(0, 8);
|
||||
frontendGitCommitHash = this.stateService.env.GIT_COMMIT_HASH;
|
||||
packetJsonVersion = this.stateService.env.PACKAGE_JSON_VERSION;
|
||||
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
||||
sponsors = null;
|
||||
contributors = null;
|
||||
requestSubscription: Subscription | undefined;
|
||||
showNavigateToSponsor = false;
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private seoService: SeoService,
|
||||
private stateService: StateService,
|
||||
private formBuilder: FormBuilder,
|
||||
private apiService: ApiService,
|
||||
private sanitizer: DomSanitizer,
|
||||
private router: Router,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
@@ -44,59 +35,15 @@ export class AboutComponent implements OnInit, OnDestroy {
|
||||
this.seoService.setTitle($localize`:@@004b222ff9ef9dd4771b777950ca1d0e4cd4348a:About`);
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.donationForm = this.formBuilder.group({
|
||||
amount: [0.01, [Validators.min(0.001), Validators.required]],
|
||||
handle: [''],
|
||||
});
|
||||
|
||||
this.paymentForm = this.formBuilder.group({
|
||||
'method': 'chain'
|
||||
});
|
||||
|
||||
this.apiService.getDonation$()
|
||||
.subscribe((sponsors) => {
|
||||
this.sponsors = sponsors;
|
||||
});
|
||||
|
||||
this.apiService.getContributor$()
|
||||
.subscribe((contributors) => {
|
||||
this.contributors = contributors;
|
||||
});
|
||||
this.sponsors$ = this.apiService.getDonation$();
|
||||
this.contributors$ = this.apiService.getContributor$();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.requestSubscription) {
|
||||
this.requestSubscription.unsubscribe();
|
||||
sponsor() {
|
||||
if (this.officialMempoolSpace) {
|
||||
this.router.navigateByUrl('/sponsor');
|
||||
} else {
|
||||
this.showNavigateToSponsor = true;
|
||||
}
|
||||
}
|
||||
|
||||
submitDonation() {
|
||||
if (this.donationForm.invalid) {
|
||||
return;
|
||||
}
|
||||
this.requestSubscription = this.apiService.requestDonation$(
|
||||
this.donationForm.get('amount').value,
|
||||
this.donationForm.get('handle').value
|
||||
)
|
||||
.pipe(
|
||||
tap((response) => {
|
||||
this.donationObj = response;
|
||||
this.donationStatus = 3;
|
||||
}),
|
||||
switchMap(() => this.apiService.checkDonation$(this.donationObj.id)
|
||||
.pipe(
|
||||
retryWhen((errors) => errors.pipe(delay(2000)))
|
||||
)
|
||||
)
|
||||
).subscribe(() => {
|
||||
this.donationStatus = 4;
|
||||
if (this.donationForm.get('handle').value) {
|
||||
this.sponsors.unshift({ handle: this.donationForm.get('handle').value });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bypassSecurityTrustUrl(text: string): SafeUrl {
|
||||
return this.sanitizer.bypassSecurityTrustUrl(text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<div class="container-xl">
|
||||
<h1 class="float-left" i18n="shared.address">Address</h1>
|
||||
<a [routerLink]="['/address/' | relativeUrl, addressString]" style="line-height: 56px; margin-left: 10px;">
|
||||
<span class="d-inline d-lg-none">{{ addressString | shortenString : 18 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ addressString }}</span>
|
||||
</a>
|
||||
<app-clipboard [text]="addressString"></app-clipboard>
|
||||
<h1 i18n="shared.address">Address</h1>
|
||||
<div class="tx-link">
|
||||
<a [routerLink]="['/address/' | relativeUrl, addressString]" >
|
||||
<span class="d-inline d-lg-none">{{ addressString | shortenString : 18 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ addressString }}</span>
|
||||
</a>
|
||||
<app-clipboard [text]="addressString"></app-clipboard>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
@@ -13,7 +15,7 @@
|
||||
<div class="box">
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="col-md">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<ng-template [ngIf]="!address.electrum">
|
||||
@@ -28,13 +30,13 @@
|
||||
</ng-template>
|
||||
<tr>
|
||||
<td i18n="address.balance">Balance</td>
|
||||
<td *ngIf="address.chain_stats.funded_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="receieved - sent" [noFiat]="true"></app-amount> (<app-fiat [value]="receieved - sent"></app-fiat>)</td>
|
||||
<td *ngIf="address.chain_stats.funded_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="receieved - sent" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="receieved - sent"></app-fiat></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="w-100 d-block d-md-none"></div>
|
||||
<div class="col qrcode-col">
|
||||
<div class="col-md qrcode-col">
|
||||
<div class="qr-wrapper">
|
||||
<app-qrcode [data]="address.address"></app-qrcode>
|
||||
</div>
|
||||
@@ -78,6 +80,11 @@
|
||||
</ng-container>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="retryLoadmore">
|
||||
<br>
|
||||
<button type="button" class="btn btn-outline-info btn-sm" (click)="loadMore()"><fa-icon [icon]="['fas', 'redo-alt']" [fixedWidth]="true"></fa-icon></button>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
@@ -114,10 +121,13 @@
|
||||
<div class="text-center">
|
||||
<span i18n="address.error.loading-address-data">Error loading address data.</span>
|
||||
<br>
|
||||
<i>{{ error.error }}</i>
|
||||
<ng-template [ngIf]="error.status === 413 || error.status === 405">
|
||||
<br><br>
|
||||
Consider viewing this address on the official Mempool website instead:
|
||||
<ng-template #displayServerError><i>{{ error.error }}</i></ng-template>
|
||||
<ng-template [ngIf]="error.status === 413 || error.status === 405" [ngIfElse]="displayServerError">
|
||||
<ng-container i18n="Electrum server limit exceeded error">
|
||||
<i>The number of transactions on this address exceeds the Electrum server limit</i>
|
||||
<br><br>
|
||||
Consider viewing this address on the official Mempool website instead:
|
||||
</ng-container>
|
||||
<br>
|
||||
<a href="https://mempool.space/address/{{ addressString }}" target="_blank">https://mempool.space/address/{{ addressString }}</a>
|
||||
<br>
|
||||
|
||||
@@ -3,21 +3,76 @@
|
||||
padding: 10px;
|
||||
padding-bottom: 5px;
|
||||
display: inline-block;
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.qrcode-col {
|
||||
text-align: right;
|
||||
.qrcode-col {
|
||||
margin: 20px auto 10px;
|
||||
text-align: center;
|
||||
@media (min-width: 992px){
|
||||
margin: 0px auto 0px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 575.98px) {
|
||||
.qrcode-col {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qrcode-col > div {
|
||||
margin-top: 20px;
|
||||
margin-right: 0px;
|
||||
.fiat {
|
||||
display: block;
|
||||
@media (min-width: 992px){
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
tr td {
|
||||
&:last-child {
|
||||
text-align: right;
|
||||
@media (min-width: 576px) {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
@media (min-width: 576px) {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.address-link {
|
||||
line-height: 56px;
|
||||
margin-left: 0px;
|
||||
top: -2px;
|
||||
position: relative;
|
||||
@media (min-width: 768px) {
|
||||
line-height: 69px;
|
||||
}
|
||||
}
|
||||
|
||||
.row{
|
||||
flex-direction: column;
|
||||
@media (min-width: 576px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.mobile-bottomcol {
|
||||
margin-top: 15px;
|
||||
}
|
||||
.details-table td:first-child {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.tx-link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
top: 14px;
|
||||
position: relative;
|
||||
@media (min-width: 768px) {
|
||||
top: 20px;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
isLoadingAddress = true;
|
||||
transactions: Transaction[];
|
||||
isLoadingTransactions = true;
|
||||
retryLoadmore = false;
|
||||
error: any;
|
||||
mainSubscription: Subscription;
|
||||
addressLoadingStatus$: Observable<number>;
|
||||
@@ -183,12 +184,17 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
this.isLoadingTransactions = true;
|
||||
this.retryLoadmore = false;
|
||||
this.electrsApiService.getAddressTransactionsFromHash$(this.address.address, this.lastTransactionTxId)
|
||||
.subscribe((transactions: Transaction[]) => {
|
||||
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
|
||||
this.loadedConfirmedTxCount += transactions.length;
|
||||
this.transactions = this.transactions.concat(transactions);
|
||||
this.isLoadingTransactions = false;
|
||||
},
|
||||
(error) => {
|
||||
this.isLoadingTransactions = false;
|
||||
this.retryLoadmore = true;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<ng-container *ngIf="!noFiat && (viewFiat$ | async) && (conversions$ | async) as conversions; else viewFiatVin">
|
||||
<span>{{ conversions.USD * (satoshis / 100000000) | currency:'USD':'symbol':'1.2-2' }}</span>
|
||||
<span class="fiat">{{ conversions.USD * (satoshis / 100000000) | currency:'USD':'symbol':'1.2-2' }}</span>
|
||||
</ng-container>
|
||||
<ng-template #viewFiatVin>
|
||||
<ng-template [ngIf]="network === 'liquid' && (satoshis === undefined || satoshis === null)" [ngIfElse]="default">
|
||||
@@ -7,8 +7,8 @@
|
||||
</ng-template>
|
||||
<ng-template #default>
|
||||
‎{{ satoshis / 100000000 | number : digitsInfo }}
|
||||
<ng-template [ngIf]="network === 'liquid'">L-</ng-template>
|
||||
<span class="symbol"><ng-template [ngIf]="network === 'liquid'">L-</ng-template>
|
||||
<ng-template [ngIf]="network === 'testnet'">t</ng-template>
|
||||
<ng-template [ngIf]="network === 'signet'">s</ng-template>BTC
|
||||
<ng-template [ngIf]="network === 'signet'">s</ng-template>BTC</span>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
.green-color {
|
||||
color: #3bcc49;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,3 +14,59 @@ tr {
|
||||
.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
li.nav-item {
|
||||
width: 100%;
|
||||
@media (min-width: 676px){
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
border-bottom: 1px solid #fff;
|
||||
@media (min-width: 676px){
|
||||
border-bottom: 1px solid #11131f;
|
||||
}
|
||||
}
|
||||
|
||||
.code-tab {
|
||||
width: auto;
|
||||
margin: 20px auto 10px;
|
||||
li.nav-item {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.code {
|
||||
.tab-content {
|
||||
padding: 0px;
|
||||
}
|
||||
.nav-tabs .nav-link.active {
|
||||
border-bottom: 1px solid #11131f;
|
||||
}
|
||||
.subtitle {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
.description {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.title {
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.subtitle {
|
||||
font-weight: bold;
|
||||
}
|
||||
.divider {
|
||||
width: 100%;
|
||||
margin: 30px auto;
|
||||
height: 1px;
|
||||
background: #333;
|
||||
}
|
||||
.websocket {
|
||||
padding: 15px;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user