mirror of
https://github.com/mempool/mempool.git
synced 2025-01-17 18:52:34 +01:00
Merge pull request #381 from mempool/simon/bisq-dashboard
Bisq dashboard
This commit is contained in:
commit
5d1af0a86e
@ -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"
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ 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 transactions: BisqTransaction[] = [];
|
||||
@ -98,7 +98,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 +126,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);
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -96,6 +96,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 => {
|
||||
|
@ -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) {
|
||||
|
49
frontend/package-lock.json
generated
49
frontend/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mempool-frontend",
|
||||
"version": "2.2.0-dev",
|
||||
"version": "2.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mempool-frontend",
|
||||
"version": "2.2.0-dev",
|
||||
"version": "2.0.0",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"dependencies": {
|
||||
"@angular/animations": "~11.2.8",
|
||||
@ -24,7 +24,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.1",
|
||||
"@ng-bootstrap/ng-bootstrap": "^7.0.0",
|
||||
"@nguniversal/express-engine": "11.2.1",
|
||||
"@types/qrcode": "^1.3.4",
|
||||
@ -33,6 +33,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",
|
||||
@ -2192,10 +2193,10 @@
|
||||
"node": ">=4.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mempool/mempool.js": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mempool/mempool.js/-/mempool.js-2.2.0.tgz",
|
||||
"integrity": "sha512-emBbMmLQd/x+4DQVno9zq4nGA9rOMAinYTOzI4s5lLVBzGL8++8JpkXouH05HC5wOHA0VpRhBX7X+lOX/o0oAA==",
|
||||
"node_modules/@mempool/mempool-js": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@mempool/mempool-js/-/mempool-js-2.2.1.tgz",
|
||||
"integrity": "sha512-zMoqXx+PgL59iQn4fEPgvYgxBi+kNNXVU99v0E8kxQXJkX9KzSVDPFlrHoaodXzGHGB2cZPMlK35okK7+LsYiw==",
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"ws": "^7.4.3"
|
||||
@ -7630,6 +7631,11 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fancy-canvas": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-0.2.2.tgz",
|
||||
"integrity": "sha512-50qi8xA0QkHbjmb8h7XQ6k2fvD7y/yMfiUw9YTarJ7rWrq6o5/3CCXPouYk+XSLASvvxtjyiQLRBFt3qkE3oyA=="
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@ -10529,6 +10535,14 @@
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lightweight-charts": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-3.3.0.tgz",
|
||||
"integrity": "sha512-W5jeBrXcHG8eHnIQ0L2CB9TLkrrsjNPlQq5SICPO8PnJ3dJ8jZkLCAwemZ7Ym7ZGCfKCz6ow1EPbyzNYxblnkw==",
|
||||
"dependencies": {
|
||||
"fancy-canvas": "0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/limiter": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
|
||||
@ -22186,10 +22200,10 @@
|
||||
"resolved": "https://registry.npmjs.org/@mempool/chartist/-/chartist-0.11.4.tgz",
|
||||
"integrity": "sha512-wSemsw2NIWS7/SHxjDe9upSdUETxNRebY0ByaJzcONKUzJSUzMuSNmKEdD3kr/g02H++JvsXR2znLC6tYEAbPA=="
|
||||
},
|
||||
"@mempool/mempool.js": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mempool/mempool.js/-/mempool.js-2.2.0.tgz",
|
||||
"integrity": "sha512-emBbMmLQd/x+4DQVno9zq4nGA9rOMAinYTOzI4s5lLVBzGL8++8JpkXouH05HC5wOHA0VpRhBX7X+lOX/o0oAA==",
|
||||
"@mempool/mempool-js": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@mempool/mempool-js/-/mempool-js-2.2.1.tgz",
|
||||
"integrity": "sha512-zMoqXx+PgL59iQn4fEPgvYgxBi+kNNXVU99v0E8kxQXJkX9KzSVDPFlrHoaodXzGHGB2cZPMlK35okK7+LsYiw==",
|
||||
"requires": {
|
||||
"axios": "^0.21.1",
|
||||
"ws": "^7.4.3"
|
||||
@ -26734,6 +26748,11 @@
|
||||
"object-keys": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"fancy-canvas": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-0.2.2.tgz",
|
||||
"integrity": "sha512-50qi8xA0QkHbjmb8h7XQ6k2fvD7y/yMfiUw9YTarJ7rWrq6o5/3CCXPouYk+XSLASvvxtjyiQLRBFt3qkE3oyA=="
|
||||
},
|
||||
"fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@ -28995,6 +29014,14 @@
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"lightweight-charts": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-3.3.0.tgz",
|
||||
"integrity": "sha512-W5jeBrXcHG8eHnIQ0L2CB9TLkrrsjNPlQq5SICPO8PnJ3dJ8jZkLCAwemZ7Ym7ZGCfKCz6ow1EPbyzNYxblnkw==",
|
||||
"requires": {
|
||||
"fancy-canvas": "0.2.2"
|
||||
}
|
||||
},
|
||||
"limiter": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
|
||||
|
@ -66,6 +66,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",
|
||||
|
@ -16,8 +16,9 @@ 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';
|
||||
|
||||
const routes: Routes = [
|
||||
let routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: MasterPageComponent,
|
||||
@ -283,6 +284,18 @@ 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'
|
||||
|
@ -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,7 +45,7 @@ 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, faSort, faChevronDown } from '@fortawesome/free-solid-svg-icons';
|
||||
import { ApiDocsComponent } from './components/api-docs/api-docs.component';
|
||||
import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component';
|
||||
import { StorageService } from './services/storage.service';
|
||||
@ -55,6 +56,7 @@ import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||
AppComponent,
|
||||
AboutComponent,
|
||||
MasterPageComponent,
|
||||
BisqMasterPageComponent,
|
||||
TelevisionComponent,
|
||||
BlockchainComponent,
|
||||
StartComponent,
|
||||
@ -127,5 +129,6 @@ export class AppModule {
|
||||
library.addIcons(faExchangeAlt);
|
||||
library.addIcons(faAngleDoubleUp);
|
||||
library.addIcons(faAngleDoubleDown);
|
||||
library.addIcons(faChevronDown);
|
||||
}
|
||||
}
|
||||
|
@ -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,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) => {
|
||||
|
@ -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',
|
||||
@ -25,6 +26,7 @@ export class BisqBlocksComponent implements OnInit {
|
||||
paginationMaxSize = 10;
|
||||
|
||||
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,60 @@
|
||||
<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>
|
||||
|
||||
<h1>
|
||||
<ng-template [ngIf]="stateService.env.OFFICIAL_BISQ_MARKETS" [ngIfElse]="nonOfficialMarkets" i18n="Bisq markets all">Markets</ng-template>
|
||||
<ng-template #nonOfficialMarkets i18n="Bisq Bitcoin markets">Bitcoin markets</ng-template>
|
||||
</h1>
|
||||
<ng-container *ngIf="{ value: (tickers$ | async) } as tickers">
|
||||
|
||||
<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 }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br><br>
|
||||
|
||||
<h2 i18n="Latest trades header">Latest trades</h2>
|
||||
<app-bisq-trades [trades$]="trades$"></app-bisq-trades>
|
||||
|
||||
</ng-container>
|
||||
</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,10 @@
|
||||
#volumeHolder {
|
||||
height: 500px;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.loadingVolumes {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
z-index: 100;
|
||||
}
|
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']);
|
||||
}
|
||||
}
|
113
frontend/src/app/bisq/bisq-market/bisq-market.component.html
Normal file
113
frontend/src/app/bisq/bisq-market/bisq-market.component.html
Normal file
@ -0,0 +1,113 @@
|
||||
<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="float-left">
|
||||
<span 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>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="radioGroupForm" class="mb-3 float-right">
|
||||
<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>
|
||||
|
||||
<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 }} {{ market.rsymbol }}</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 }} {{ market.lsymbol }}</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 }} {{ market.rsymbol }}</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
14
frontend/src/app/bisq/bisq-market/bisq-market.component.scss
Normal file
14
frontend/src/app/bisq/bisq-market/bisq-market.component.scss
Normal file
@ -0,0 +1,14 @@
|
||||
.priceheader {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.loadingChart {
|
||||
z-index: 100;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
#graphHolder {
|
||||
height: 550px;
|
||||
}
|
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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) => {
|
||||
|
44
frontend/src/app/bisq/bisq-trades/bisq-trades.component.html
Normal file
44
frontend/src/app/bisq/bisq-trades/bisq-trades.component.html
Normal file
@ -0,0 +1,44 @@
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<th i18n>Date</th>
|
||||
<th 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>
|
||||
<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 }} {{ (trade._market || market).rsymbol }}</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 }} {{ (trade._market || market).lsymbol }}</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 }} {{ (trade._market || market).rsymbol }}</ng-template>
|
||||
</td>
|
||||
</ng-template>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<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>
|
||||
|
||||
<ng-template #amount let-i i18n="Trade amount (Symbol)">Amount ({{ i }})</ng-template>
|
13
frontend/src/app/bisq/bisq-trades/bisq-trades.component.ts
Normal file
13
frontend/src/app/bisq/bisq-trades/bisq-trades.component.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, 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 {
|
||||
@Input() trades$: Observable<any>;
|
||||
@Input() market: any;
|
||||
}
|
@ -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;
|
||||
|
@ -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',
|
||||
@ -65,6 +66,7 @@ export class BisqTransactionsComponent implements OnInit {
|
||||
'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({
|
||||
|
@ -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,14 @@ 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 { 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 +18,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 +34,14 @@ import { BsqAmountComponent } from './bsq-amount/bsq-amount.component';
|
||||
BisqTransactionDetailsComponent,
|
||||
BisqTransfersComponent,
|
||||
BisqBlocksComponent,
|
||||
BisqExplorerComponent,
|
||||
BisqAddressComponent,
|
||||
BisqStatsComponent,
|
||||
BsqAmountComponent,
|
||||
LightweightChartsComponent,
|
||||
LightweightChartsAreaComponent,
|
||||
BisqDashboardComponent,
|
||||
BisqMarketComponent,
|
||||
BisqTradesComponent,
|
||||
],
|
||||
imports: [
|
||||
BisqRoutingModule,
|
||||
|
@ -5,55 +5,58 @@ 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';
|
||||
|
||||
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: 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: '**',
|
||||
redirectTo: ''
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -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,133 @@
|
||||
import { createChart, CrosshairMode, isBusinessDay } from 'lightweight-charts';
|
||||
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDestroy } 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 OnChanges, OnDestroy {
|
||||
@Input() data: any;
|
||||
@Input() lineData: any;
|
||||
@Input() precision: number;
|
||||
|
||||
areaSeries: any;
|
||||
volumeSeries: any;
|
||||
chart: any;
|
||||
lineSeries: any;
|
||||
container: any;
|
||||
|
||||
width = 1110;
|
||||
height = 500;
|
||||
|
||||
constructor(
|
||||
private element: ElementRef,
|
||||
) {
|
||||
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';
|
||||
});
|
||||
}
|
||||
|
||||
businessDayToString(businessDay) {
|
||||
return businessDay.year + '-' + businessDay.month + '-' + businessDay.day;
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
if (!this.data) {
|
||||
return;
|
||||
}
|
||||
this.areaSeries.setData(this.data);
|
||||
this.lineSeries.setData(this.lineData);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.chart.remove();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
import { createChart, CrosshairMode } from 'lightweight-charts';
|
||||
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDestroy } 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 OnChanges, OnDestroy {
|
||||
@Input() data: any;
|
||||
@Input() volumeData: any;
|
||||
@Input() precision: number;
|
||||
|
||||
lineSeries: any;
|
||||
volumeSeries: any;
|
||||
chart: any;
|
||||
|
||||
constructor(
|
||||
private element: ElementRef,
|
||||
) {
|
||||
this.chart = createChart(this.element.nativeElement, {
|
||||
width: 1110,
|
||||
height: 500,
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
if (!this.data) {
|
||||
return;
|
||||
}
|
||||
this.lineSeries.setData(this.data);
|
||||
this.volumeSeries.setData(this.volumeData);
|
||||
|
||||
this.lineSeries.applyOptions({
|
||||
priceFormat: {
|
||||
type: 'price',
|
||||
precision: this.precision,
|
||||
minMove: 0.0000001,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.chart.remove();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
||||
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
|
||||
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
|
||||
<img [src]="'./resources/bisq-markets.svg'" height="35" width="180" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }">
|
||||
<div class="connection-badge">
|
||||
<div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
|
||||
<div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</a>
|
||||
|
||||
<div class="navbar-collapse" id="navbarCollapse">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
|
||||
<a class="nav-link" [routerLink]="['/' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tachometer-alt']" [fixedWidth]="true" i18n-title="master-page.dashboard" title="Dashboard"></fa-icon></a>
|
||||
</li>
|
||||
<li class="nav-item mr-2" routerLinkActive="active">
|
||||
<a class="nav-link" [routerLink]="['/api' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'cogs']" [fixedWidth]="true" i18n-title="master-page.api" title="API"></fa-icon></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<br />
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
<br>
|
@ -0,0 +1,85 @@
|
||||
li.nav-item.active {
|
||||
background-color: #653b9c;
|
||||
}
|
||||
|
||||
fa-icon {
|
||||
font-size: 1.66em;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
li.nav-item {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.navbar {
|
||||
padding: 0rem 2rem;
|
||||
}
|
||||
fa-icon {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.dropdown-container {
|
||||
margin-right: 16px;
|
||||
}
|
||||
li.nav-item {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
li.nav-item a {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
nav {
|
||||
box-shadow: 0px 0px 15px 0px #000;
|
||||
}
|
||||
|
||||
.connection-badge {
|
||||
position: absolute;
|
||||
top: 13px;
|
||||
left: 0px;
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
margin: 0 auto;
|
||||
display: table;
|
||||
}
|
||||
|
||||
.mainnet.active {
|
||||
background-color: #653b9c;
|
||||
}
|
||||
|
||||
.liquid.active {
|
||||
background-color: #116761;
|
||||
}
|
||||
|
||||
.testnet.active {
|
||||
background-color: #1d486f;
|
||||
}
|
||||
|
||||
.signet.active {
|
||||
background-color: #6f1d5d;
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
border-top: 1px solid #121420;
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
vertical-align: 0.1em;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items:center;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-master-page',
|
||||
templateUrl: './bisq-master-page.component.html',
|
||||
styleUrls: ['./bisq-master-page.component.scss'],
|
||||
})
|
||||
export class BisqMasterPageComponent implements OnInit {
|
||||
connectionState$: Observable<number>;
|
||||
navCollapsed = false;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.connectionState$ = this.stateService.connectionState$;
|
||||
}
|
||||
|
||||
collapse(): void {
|
||||
this.navCollapsed = !this.navCollapsed;
|
||||
}
|
||||
}
|
@ -27,21 +27,21 @@
|
||||
|
||||
<div class="navbar-collapse" id="navbarCollapse">
|
||||
<ul class="navbar-nav {{ network.val }}">
|
||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
|
||||
<a class="nav-link" [routerLink]="['/' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tachometer-alt']" [fixedWidth]="true" i18n-title="master-page.dashboard" title="Dashboard"></fa-icon></a>
|
||||
</li>
|
||||
<ng-template [ngIf]="network.val === 'bisq'" [ngIfElse]="notBisq">
|
||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
|
||||
<a class="nav-link" [routerLink]="['/bisq']" (click)="collapse()"><fa-icon [icon]="['fas', 'list']" [fixedWidth]="true" i18n-title="master-page.transactions" title="Transactions"></fa-icon></a>
|
||||
<a class="nav-link" [routerLink]="['/bisq/transactions']" (click)="collapse()"><fa-icon [icon]="['fas', 'list']" [fixedWidth]="true" i18n-title="master-page.transactions" title="Transactions"></fa-icon></a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" [routerLink]="['/bisq/blocks']" (click)="collapse()"><fa-icon [icon]="['fas', 'cubes']" [fixedWidth]="true" i18n-title="master-page.blocks" title="Blocks"></fa-icon></a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" [routerLink]="['/bisq/stats']" (click)="collapse()"><fa-icon [icon]="['fas', 'tachometer-alt']" [fixedWidth]="true" i18n-title="master-page.stats" title="Stats"></fa-icon></a>
|
||||
<a class="nav-link" [routerLink]="['/bisq/stats']" (click)="collapse()"><fa-icon [icon]="['fas', 'file-alt']" [fixedWidth]="true" i18n-title="master-page.stats" title="Stats"></fa-icon></a>
|
||||
</li>
|
||||
</ng-template>
|
||||
<ng-template #notBisq>
|
||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
|
||||
<a class="nav-link" [routerLink]="['/' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tachometer-alt']" [fixedWidth]="true" i18n-title="master-page.dashboard" title="Dashboard"></fa-icon></a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" [routerLink]="['/blocks' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'cubes']" [fixedWidth]="true" i18n-title="master-page.blocks" title="Blocks"></fa-icon></a>
|
||||
</li>
|
||||
|
@ -21,6 +21,7 @@ export interface WebsocketResponse {
|
||||
'track-address'?: string;
|
||||
'track-asset'?: string;
|
||||
'watch-mempool'?: boolean;
|
||||
'track-bisq-market'?: string;
|
||||
}
|
||||
|
||||
export interface MempoolBlock {
|
||||
|
@ -24,6 +24,7 @@ export interface Env {
|
||||
ITEMS_PER_PAGE: number;
|
||||
KEEP_BLOCKS_AMOUNT: number;
|
||||
OFFICIAL_MEMPOOL_SPACE: boolean;
|
||||
OFFICIAL_BISQ_MARKETS: boolean;
|
||||
NGINX_PROTOCOL?: string;
|
||||
NGINX_HOSTNAME?: string;
|
||||
NGINX_PORT?: string;
|
||||
@ -35,6 +36,7 @@ const defaultEnv: Env = {
|
||||
'TESTNET_ENABLED': false,
|
||||
'SIGNET_ENABLED': false,
|
||||
'LIQUID_ENABLED': false,
|
||||
'OFFICIAL_BISQ_MARKETS': false,
|
||||
'BISQ_ENABLED': false,
|
||||
'BISQ_SEPARATE_BACKEND': false,
|
||||
'ITEMS_PER_PAGE': 10,
|
||||
|
@ -23,7 +23,7 @@ export class WebsocketService {
|
||||
|
||||
private websocketSubject: WebSocketSubject<WebsocketResponse>;
|
||||
private goneOffline = false;
|
||||
private lastWant: string[] | null = null;
|
||||
private lastWant: string | null = null;
|
||||
private isTrackingTx = false;
|
||||
private latestGitCommit = '';
|
||||
private onlineCheckTimeout: number;
|
||||
@ -95,7 +95,7 @@ export class WebsocketService {
|
||||
if (this.goneOffline === true) {
|
||||
this.goneOffline = false;
|
||||
if (this.lastWant) {
|
||||
this.want(this.lastWant, true);
|
||||
this.want(JSON.parse(this.lastWant), true);
|
||||
}
|
||||
this.stateService.connectionState$.next(2);
|
||||
}
|
||||
@ -150,6 +150,14 @@ export class WebsocketService {
|
||||
this.websocketSubject.next({ 'track-asset': 'stop' });
|
||||
}
|
||||
|
||||
startTrackBisqMarket(market: string) {
|
||||
this.websocketSubject.next({ 'track-bisq-market': market });
|
||||
}
|
||||
|
||||
stopTrackingBisqMarket() {
|
||||
this.websocketSubject.next({ 'track-bisq-market': 'stop' });
|
||||
}
|
||||
|
||||
fetchStatistics(historicalDate: string) {
|
||||
this.websocketSubject.next({ historicalDate });
|
||||
}
|
||||
@ -158,11 +166,11 @@ export class WebsocketService {
|
||||
if (!this.stateService.isBrowser) {
|
||||
return;
|
||||
}
|
||||
if (data === this.lastWant && !force) {
|
||||
if (JSON.stringify(data) === this.lastWant && !force) {
|
||||
return;
|
||||
}
|
||||
this.websocketSubject.next({action: 'want', data: data});
|
||||
this.lastWant = data;
|
||||
this.lastWant = JSON.stringify(data);
|
||||
}
|
||||
|
||||
goOffline() {
|
||||
|
File diff suppressed because it is too large
Load Diff
17
frontend/src/resources/bisq-markets.svg
Normal file
17
frontend/src/resources/bisq-markets.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="172.649" height="33.558" viewBox="0 0 172.649 33.558">
|
||||
<g id="bisq-markets" transform="translate(-158 -373.5)">
|
||||
<g id="bisq_logo_green" transform="translate(158 376.985)">
|
||||
<g id="bisq_mark" transform="translate(0 0)">
|
||||
<path id="Combined_Shape" data-name="Combined Shape" d="M8.251,22.938A11.845,11.845,0,0,1,0,11.722,11.38,11.38,0,0,1,3.118,3.8,11.625,11.625,0,0,1,10.946,0c4.083,0,5.907,3.373,4.469,5.255-1.607,2.092-3.475,1.922-4.865,1.025,0,0-1.912-1.281-2.476-1.708-.869-.64-1.825-.64-2.172.385C5.294,6.792,4.991,7.945,4.513,9.78A12.5,12.5,0,0,0,4.166,12.6a5.734,5.734,0,0,0,2.215,4.483,23.065,23.065,0,0,0,3.084,2.305,5.649,5.649,0,0,0,2.954.9h.087a5.649,5.649,0,0,0,2.954-.9,37.207,37.207,0,0,0,4.083-2.69c1.611-1.5,3.849.178,2.286,2.187a12.238,12.238,0,0,1-9.679,4.684A12.38,12.38,0,0,1,8.251,22.938Zm3.874-4.88a.512.512,0,0,1-.334-.111c-.251-.221-.71-.664-1.128-1.034a.368.368,0,0,1-.057-.44.472.472,0,0,1,.432-.224,22.129,22.129,0,0,1,2.213,0,.446.446,0,0,1,.381.251.362.362,0,0,1-.089.413c-.417.369-.876.812-1.085,1.034a.511.511,0,0,1-.322.111Zm2.482-4.744a1.463,1.463,0,0,1,1.473-1.377c.332,0,1.368.12,1.553.561.369.8-.888,1.883-1.443,2.084a1.728,1.728,0,0,1-.575.11C14.852,14.691,14.525,14,14.607,13.314ZM8.031,14.581c-.555-.2-1.812-1.282-1.443-2.084.148-.441,1.221-.561,1.554-.561a1.463,1.463,0,0,1,1.473,1.377C9.7,14,9.37,14.691,8.606,14.691A1.728,1.728,0,0,1,8.031,14.581Zm10.457-3.794C16.284,9.8,14.689,9.1,16,7.458a16.235,16.235,0,0,1,2.164-2.219c.851-.7,1.378-1.137,2.108.328A30.626,30.626,0,0,1,21.959,9.43a1.372,1.372,0,0,1-1.431,1.894A5.117,5.117,0,0,1,18.488,10.787Z" transform="translate(0 0)" fill="#25b135"/>
|
||||
</g>
|
||||
<g id="Group" transform="translate(27.192 0.604)">
|
||||
<path id="Fill_1" data-name="Fill 1" d="M14.5,20.545H10.918V14.184a5.512,5.512,0,0,1-1.692.958,5.913,5.913,0,0,1-1.975.354,7.037,7.037,0,0,1-5.1-2.158A7.954,7.954,0,0,1,0,7.748a7.954,7.954,0,0,1,2.153-5.59A7.037,7.037,0,0,1,7.251,0c.128,0,.26,0,.4.012.1.005.507.048.524.049a7.027,7.027,0,0,1,4.171,2.1A7.939,7.939,0,0,1,14.5,7.572c0,.013,0,12.439,0,12.968v.006ZM7.146,3.486h0c-2.682.027-3.638,2.219-3.638,4.261,0,2.061.969,4.262,3.688,4.262,2.156,0,3.781-1.832,3.781-4.262,0-.081,0-.165-.006-.249v0a4.727,4.727,0,0,0-.754-2.431A3.448,3.448,0,0,0,8.073,3.592v0c-.062-.015-.128-.03-.2-.043l-.011,0-.024,0-.034-.005-.026,0a4.62,4.62,0,0,0-.636-.05Z" transform="translate(33.235 4.834)" fill="#25b135"/>
|
||||
<path id="Fill_4" data-name="Fill 4" d="M6.629,6.112,5.159,5.789c-.964-.206-1.442-.627-1.42-1.252.023-.662.766-1.15,1.726-1.135A1.817,1.817,0,0,1,7.376,4.459l2.842-1.873A5.337,5.337,0,0,0,5.585,0a6.052,6.052,0,0,0-3.6,1.1A4.308,4.308,0,0,0,.156,4.48C.076,6.743,1.579,8.46,4.279,9.19c.41.111.951.252,1.495.358,1.271.246,1.432.7,1.416,1.161-.028.8-.766,1.3-1.879,1.281a3.207,3.207,0,0,1-2.931-1.768L0,12.326a5.623,5.623,0,0,0,5.19,3.067c2.986.048,5.49-2.027,5.582-4.626.083-2.348-1.386-4-4.143-4.655" transform="translate(21.452 4.937)" fill="#25b135"/>
|
||||
<path id="Fill_6" data-name="Fill 6" d="M0,14.778H3.695V0H0Z" transform="translate(16.218 5.245)" fill="#25b135"/>
|
||||
<path id="Fill_8" data-name="Fill 8" d="M7.252,20.545h0c-.128,0-.26,0-.4-.012-.1-.005-.207-.014-.311-.023H6.529L6.451,20.5l-.127-.011v-.008a7.025,7.025,0,0,1-4.171-2.1A7.936,7.936,0,0,1,0,12.983a.036.036,0,0,0,0-.008.033.033,0,0,1,0-.011V0H3.585V6.362A5.512,5.512,0,0,1,5.276,5.4,5.913,5.913,0,0,1,7.251,5.05a7.037,7.037,0,0,1,5.1,2.158A7.952,7.952,0,0,1,14.5,12.8a7.952,7.952,0,0,1-2.153,5.59A7.037,7.037,0,0,1,7.252,20.545Zm-.823-3.6h0c.059.015.123.029.2.043l.012,0,.023,0L6.694,17l.026,0a4.527,4.527,0,0,0,.636.05c2.683-.027,3.638-2.219,3.638-4.261,0-2.061-.969-4.262-3.688-4.262-2.156,0-3.781,1.832-3.781,4.262a5.138,5.138,0,0,0,.111,1.051h-.01a4.351,4.351,0,0,0,.9,1.95,3.42,3.42,0,0,0,1.905,1.156v0Z" transform="translate(0 0)" fill="#25b135"/>
|
||||
</g>
|
||||
</g>
|
||||
<path id="Path_3" data-name="Path 3" d="M.924,0H1.98V-6.4H2L4.631,0h.7L7.964-6.4h.022V0H9.042V-7.788H7.469l-2.453,5.9H4.972L2.5-7.788H.924Zm11.8,0h1.232l.77-1.925h3.707L19.217,0h1.232L17.116-7.788h-.957ZM15.1-2.849l1.474-3.575H16.6l1.452,3.575ZM24.211,0h1.056V-3.432h1.177L28.424,0h1.32L27.533-3.553A2.088,2.088,0,0,0,29.447-5.61c0-.979-.506-2.178-2.5-2.178H24.211Zm1.056-6.864h1.386c.836,0,1.672.2,1.672,1.254s-.836,1.254-1.672,1.254H25.267ZM33.682,0h1.056V-3.894h.088L38.456,0H40L35.981-4.191l3.762-3.6H38.269L34.826-4.422h-.088V-7.788H33.682ZM43.56,0h5.225V-.99H44.616v-2.5h3.7v-.99h-3.7V-6.8h3.971v-.99H43.56Zm11.1,0h1.056V-6.8h2.508v-.99H52.151v.99h2.508Zm6.8-.88A2.99,2.99,0,0,0,63.943.2a2.46,2.46,0,0,0,2.706-2.409c0-2.8-3.839-1.628-3.839-3.6,0-.484.352-1.188,1.518-1.188a1.634,1.634,0,0,1,1.386.682l.858-.781a2.693,2.693,0,0,0-2.244-.891,2.365,2.365,0,0,0-2.64,2.178c0,3.036,3.839,1.925,3.839,3.718a1.443,1.443,0,0,1-1.551,1.3,1.943,1.943,0,0,1-1.65-.836Z" transform="translate(264 394.28)" fill="#9d9da5"/>
|
||||
<line id="Line_1" data-name="Line 1" y1="33.559" transform="translate(248.67 373.5)" fill="none" stroke="#3e3e50" stroke-width="1"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 5.0 KiB |
@ -8,6 +8,7 @@
|
||||
]
|
||||
},
|
||||
"array-type": false,
|
||||
"forin": false,
|
||||
"arrow-parens": false,
|
||||
"arrow-return-shorthand": true,
|
||||
"curly": true,
|
||||
|
@ -27,11 +27,7 @@
|
||||
"ENABLED": true,
|
||||
"TX_PER_SECOND_SAMPLE_PERIOD": 150
|
||||
},
|
||||
"BISQ_BLOCKS": {
|
||||
"ENABLED": true,
|
||||
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db/json"
|
||||
},
|
||||
"BISQ_MARKETS": {
|
||||
"BISQ": {
|
||||
"ENABLED": true,
|
||||
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user