Merge pull request #381 from mempool/simon/bisq-dashboard

Bisq dashboard
This commit is contained in:
wiz 2021-04-26 07:17:03 +09:00 committed by GitHub
commit 5d1af0a86e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 4143 additions and 2539 deletions

View File

@ -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"
}

View File

@ -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);
}

View File

@ -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 = {};

View File

@ -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',

View File

@ -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));
}

View File

@ -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 => {

View File

@ -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))
;
}

View File

@ -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') {

View File

@ -144,6 +144,7 @@ export interface WebsocketResponse {
'track-tx': string;
'track-address': string;
'watch-mempool': boolean;
'track-bisq-market': string;
}
export interface VbytesPerSecond {

View File

@ -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) {

View File

@ -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",

View File

@ -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",

View File

@ -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'

View File

@ -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);
}
}

View File

@ -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) => {

View File

@ -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');
}
}

View File

@ -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) => {

View File

@ -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);

View File

@ -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>

View File

@ -0,0 +1,10 @@
#volumeHolder {
height: 500px;
background-color: #000;
}
.loadingVolumes {
position: relative;
top: 50%;
z-index: 100;
}

View 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);
}
}

View File

@ -1 +0,0 @@
<router-outlet></router-outlet>

View File

@ -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']);
}
}

View 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>

View File

@ -0,0 +1,14 @@
.priceheader {
font-size: 24px;
}
.loadingChart {
z-index: 100;
position: absolute;
top: 50%;
left: 50%;
}
#graphHolder {
height: 550px;
}

View 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;
}
}
}

View File

@ -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) => {

View 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>

View 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;
}

View File

@ -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;

View File

@ -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({

View File

@ -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;
}

View File

@ -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,

View File

@ -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({

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -21,6 +21,7 @@ export interface WebsocketResponse {
'track-address'?: string;
'track-asset'?: string;
'watch-mempool'?: boolean;
'track-bisq-market'?: string;
}
export interface MempoolBlock {

View File

@ -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,

View File

@ -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

View 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

View File

@ -8,6 +8,7 @@
]
},
"array-type": false,
"forin": false,
"arrow-parens": false,
"arrow-return-shorthand": true,
"curly": true,

View File

@ -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"
}