From 88e5b0343017bdb0b8e6b485dab3185edee736d6 Mon Sep 17 00:00:00 2001 From: softsimon Date: Thu, 10 Sep 2020 14:46:23 +0700 Subject: [PATCH 1/4] Bisq Markets API. WIP --- backend/mempool-config.sample.json | 2 + backend/src/api/{ => bisq}/bisq.ts | 6 +- backend/src/api/bisq/interfaces.ts | 240 +++++++++++++++++++++++ backend/src/api/bisq/markets-api.ts | 291 ++++++++++++++++++++++++++++ backend/src/api/bisq/markets.ts | 90 +++++++++ backend/src/index.ts | 20 +- backend/src/interfaces.ts | 93 +-------- backend/src/routes.ts | 257 +++++++++++++++++++++++- 8 files changed, 905 insertions(+), 94 deletions(-) rename backend/src/api/{ => bisq}/bisq.ts (98%) create mode 100644 backend/src/api/bisq/interfaces.ts create mode 100644 backend/src/api/bisq/markets-api.ts create mode 100644 backend/src/api/bisq/markets.ts diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json index 9cb69deb2..9d8e4af37 100644 --- a/backend/mempool-config.sample.json +++ b/backend/mempool-config.sample.json @@ -15,7 +15,9 @@ "TX_PER_SECOND_SPAN_SECONDS": 150, "ELECTRS_API_URL": "http://localhost:50001", "BISQ_ENABLED": false, + "BISQ_MARKET_ENABLED": false, "BSQ_BLOCKS_DATA_PATH": "/bisq/data", + "BSQ_MARKETS_DATA_PATH": "/bisq-markets/data", "SSL": false, "SSL_CERT_FILE_PATH": "/etc/letsencrypt/live/mysite/fullchain.pem", "SSL_KEY_FILE_PATH": "/etc/letsencrypt/live/mysite/privkey.pem" diff --git a/backend/src/api/bisq.ts b/backend/src/api/bisq/bisq.ts similarity index 98% rename from backend/src/api/bisq.ts rename to backend/src/api/bisq/bisq.ts index 09f201f13..0f4cdee81 100644 --- a/backend/src/api/bisq.ts +++ b/backend/src/api/bisq/bisq.ts @@ -1,8 +1,8 @@ -const config = require('../../mempool-config.json'); +const config = require('../../../mempool-config.json'); import * as fs from 'fs'; import * as request from 'request'; -import { BisqBlocks, BisqBlock, BisqTransaction, BisqStats, BisqTrade } from '../interfaces'; -import { Common } from './common'; +import { BisqBlocks, BisqBlock, BisqTransaction, BisqStats, BisqTrade } from './interfaces'; +import { Common } from '../common'; class Bisq { private static BLOCKS_JSON_FILE_PATH = '/all/blocks.json'; diff --git a/backend/src/api/bisq/interfaces.ts b/backend/src/api/bisq/interfaces.ts new file mode 100644 index 000000000..500789ab9 --- /dev/null +++ b/backend/src/api/bisq/interfaces.ts @@ -0,0 +1,240 @@ + +export interface BisqBlocks { + chainHeight: number; + blocks: BisqBlock[]; +} + +export interface BisqBlock { + height: number; + time: number; + hash: string; + previousBlockHash: string; + txs: BisqTransaction[]; +} + +export interface BisqTransaction { + txVersion: string; + id: string; + blockHeight: number; + blockHash: string; + time: number; + inputs: BisqInput[]; + outputs: BisqOutput[]; + txType: string; + txTypeDisplayString: string; + burntFee: number; + invalidatedBsq: number; + unlockBlockHeight: number; +} + +export interface BisqStats { + minted: number; + burnt: number; + addresses: number; + unspent_txos: number; + spent_txos: number; +} + +interface BisqInput { + spendingTxOutputIndex: number; + spendingTxId: string; + bsqAmount: number; + isVerified: boolean; + address: string; + time: number; +} + +interface BisqOutput { + txVersion: string; + txId: string; + index: number; + bsqAmount: number; + btcAmount: number; + height: number; + isVerified: boolean; + burntFee: number; + invalidatedBsq: number; + address: string; + scriptPubKey: BisqScriptPubKey; + time: any; + txType: string; + txTypeDisplayString: string; + txOutputType: string; + txOutputTypeDisplayString: string; + lockTime: number; + isUnspent: boolean; + spentInfo: SpentInfo; + opReturn?: string; +} + +interface BisqScriptPubKey { + addresses: string[]; + asm: string; + hex: string; + reqSigs: number; + type: string; +} + +interface SpentInfo { + height: number; + inputIndex: number; + txId: string; +} + +export interface BisqTrade { + direction: string; + price: string; + amount: string; + volume: string; + payment_method: string; + trade_id: string; + trade_date: number; +} + +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; + 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; } + +interface OffersMarket { + buys: Offer[] | null; + sells: Offer[] | null; +} + +export interface OffsersData { + 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 { + direction: string; + price: string; + amount: string; + volume: string; + payment_method: string; + trade_id: string; + trade_date: number; +} + +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; + _tradePrice: string; + _tradeAmount: string; + _tradeVolume: string; + _offerAmount: string; +} + +export interface MarketVolume { + period_start: number; + volume: string; + num_trades: number; +} + +export interface MarketsApiError { + success: number; + error: string; +} + +export type Interval = 'minute' | 'half_hour' | 'hour' | 'half_day' | 'day' | 'week' | 'month' | 'year' | 'auto'; diff --git a/backend/src/api/bisq/markets-api.ts b/backend/src/api/bisq/markets-api.ts new file mode 100644 index 000000000..83d3e9b76 --- /dev/null +++ b/backend/src/api/bisq/markets-api.ts @@ -0,0 +1,291 @@ +import { Currencies, OffsersData, TradesData, Depth, Currency, Interval, HighLowOpenClose, + Markets, Offers, Offer, BisqTrade, MarketVolume, Tickers } from './interfaces'; + +class BisqMarketsApi { + private cryptoCurrencyData: Currency[] = []; + private fiatCurrencyData: Currency[] = []; + private offersData: OffsersData[] = []; + private tradesData: TradesData[] = []; + + constructor() { } + + public setData(cryptoCurrency: Currency[], fiatCurrency: Currency[], offers: OffsersData[], trades: TradesData[]) { + this.cryptoCurrencyData = cryptoCurrency, + this.fiatCurrencyData = fiatCurrency; + this.offersData = offers; + this.tradesData = trades; + + this.fiatCurrencyData.forEach((currency) => currency.type = 'fiat'); + this.cryptoCurrencyData.forEach((currency) => currency.type = 'crypto'); + this.tradesData.forEach((trade) => { + trade._market = trade.currencyPair.toLowerCase().replace('/', '_'); + }); + } + + getCurrencies( + type: 'crypto' | 'fiat' | 'all' = 'all', + ): Currencies { + let currencies: Currency[]; + + switch (type) { + case 'fiat': + currencies = this.fiatCurrencyData; + break; + case 'crypto': + currencies = this.cryptoCurrencyData; + break; + case 'all': + default: + currencies = this.cryptoCurrencyData.concat(this.fiatCurrencyData); + } + const result = {}; + currencies.forEach((currency) => { + result[currency.code] = currency; + }); + return result; + } + + getDepth( + market: string, + ): Depth { + const currencyPair = market.replace('_', '/').toUpperCase(); + + const buys = this.offersData + .filter((offer) => offer.currencyPair === currencyPair && offer.direction === 'BUY') + .map((offer) => offer.price) + .sort((a, b) => b - a) + .map((price) => this.intToBtc(price)); + + const sells = this.offersData + .filter((offer) => offer.currencyPair === currencyPair && offer.direction === 'SELL') + .map((offer) => offer.price) + .sort((a, b) => a - b) + .map((price) => this.intToBtc(price)); + + const result = {}; + result[market] = { + 'buys': buys, + 'sells': sells, + }; + return result; + } + + getOffers( + market: string, + direction?: 'BUY' | 'SELL', + ): Offers { + const currencyPair = market.replace('_', '/').toUpperCase(); + + let buys: Offer[] | null = null; + let sells: Offer[] | null = null; + + if (!direction || direction === 'BUY') { + buys = this.offersData + .filter((offer) => offer.currencyPair === currencyPair && offer.direction === 'BUY') + .map((offer) => this.offerDataToOffer(offer)); + } + + if (!direction || direction === 'SELL') { + sells = this.offersData + .filter((offer) => offer.currencyPair === currencyPair && offer.direction === 'SELL') + .map((offer) => this.offerDataToOffer(offer)); + } + + const result: Offers = {}; + result[market] = { + 'buys': buys, + 'sells': sells, + }; + return result; + } + + getMarkets(): Markets { + const allCurrencies = this.getCurrencies(); + const markets = {}; + + for (const currency of Object.keys(allCurrencies)) { + if (allCurrencies[currency].code === 'BTC') { + continue; + } + + const isFiat = allCurrencies[currency].type === 'fiat'; + const pmarketname = allCurrencies['BTC']['name']; + + const lsymbol = isFiat ? 'BTC' : currency; + const rsymbol = isFiat ? currency : 'BTC'; + const lname = isFiat ? pmarketname : allCurrencies[currency].name; + const rname = isFiat ? allCurrencies[currency].name : pmarketname; + const ltype = isFiat ? 'crypto' : allCurrencies[currency].type; + const rtype = isFiat ? 'fiat' : 'crypto'; + const lprecision = 8; + const rprecision = isFiat ? 2 : 8; + const pair = lsymbol.toLowerCase() + '_' + rsymbol.toLowerCase(); + + markets[pair] = { + 'pair': pair, + 'lname': lname, + 'rname': rname, + 'lsymbol': lsymbol, + 'rsymbol': rsymbol, + 'lprecision': lprecision, + 'rprecision': rprecision, + 'ltype': ltype, + 'rtype': rtype, + 'name': lname + '/' + rname, + }; + } + + return markets; + } + + getTrades( + market: string, + timestamp_from?: number, + timestamp_to?: number, + trade_id_from?: string, + trade_id_to?: string, + direction?: 'buy' | 'sell', + limit: number = 100, + sort: 'asc' | 'desc' = 'desc', + ): BisqTrade[] { + limit = Math.min(limit, 2000); + let trade_id_from_ts: number | null = null; + let trade_id_to_ts: number | null = null; + const _market = market === 'all' ? null : market; + + if (!timestamp_from) { + timestamp_from = new Date('2016-01-01').getTime(); + } + if (!timestamp_to) { + timestamp_to = new Date().getTime(); + } + + const allCurrencies = this.getCurrencies(); + + // note: the offer_id_from/to depends on iterating over trades in + // descending chronological order. + const tradesDataSorted = this.tradesData.slice(); + if (sort === 'asc') { + tradesDataSorted.reverse(); + } + + let matches: TradesData[] = []; + for (const trade of tradesDataSorted) { + if (trade_id_from === trade.offerId) { + trade_id_from_ts = trade.tradeDate; + } + if (trade_id_to === trade.offerId) { + trade_id_to_ts = trade.tradeDate; + } + if (trade_id_to && trade_id_to_ts === null) { + continue; + } + if (trade_id_from && trade_id_from_ts != null && trade_id_from_ts !== trade.tradeDate ) { + continue; + } + if (_market && _market !== trade._market) { + continue; + } + if (timestamp_from && timestamp_from > trade.tradeDate) { + continue; + } + if (timestamp_to && timestamp_to < trade.tradeDate) { + continue; + } + if (direction && direction !== trade.direction.toLowerCase() ) { + continue; + } + + // Filter out bogus trades with BTC/BTC or XXX/XXX market. + // See github issue: https://github.com/bitsquare/bitsquare/issues/883 + const currencyPairs = trade.currencyPair.split('/'); + if (currencyPairs[0] === currencyPairs[1]) { + continue; + } + + const currencyLeft = allCurrencies[currencyPairs[0]]; + const currencyRight = allCurrencies[currencyPairs[1]]; + + trade.tradePrice = trade.primaryMarketTradePrice * Math.pow(10, 8 - currencyRight.precision); + trade.tradeAmount = trade.primaryMarketTradeAmount * Math.pow(10, 8 - currencyLeft.precision); + const tradeVolume = trade.primaryMarketTradeVolume * Math.pow(10, 8 - currencyRight.precision); + + trade._tradePrice = this.intToBtc(trade.tradePrice); + trade._tradeAmount = this.intToBtc(trade.tradeAmount); + trade._tradeVolume = this.intToBtc(tradeVolume); + trade._offerAmount = this.intToBtc(trade.offerAmount); + + matches.push(trade); + + if (matches.length >= limit) { + break; + } + } + + if ((trade_id_from && !trade_id_from_ts) || (trade_id_to && !trade_id_to_ts) ) { + matches = []; + } + + return matches.map((trade) => { + return { + direction: trade.direction, + price: trade._tradePrice, + amount: trade._tradeAmount, + volume: trade._tradeVolume, + payment_method: trade.paymentMethod, + trade_id: trade.offerId, + trade_date: trade.tradeDate, + }; + }); + } + + getVolumes( + timestamp_from: number, + timestamp_to: number, + interval: Interval, + market?: string, + ): MarketVolume[] { + return []; + } + + getTicker( + market?: string, + ): Tickers { + return {}; + } + + getHloc( + market: string, + interval: Interval = 'auto', + timestamp_from?: number, + timestamp_to?: number, + ): HighLowOpenClose[] { + if (!timestamp_from) { + timestamp_from = new Date('2016-01-01').getTime(); + } + if (!timestamp_to) { + timestamp_to = new Date().getTime(); + } + return []; + } + + private offerDataToOffer(offer: OffsersData): Offer { + return { + offer_id: offer.id, + offer_date: offer.date, + direction: offer.direction, + min_amount: this.intToBtc(offer.minAmount), + amount: this.intToBtc(offer.amount), + price: this.intToBtc(offer.price), + volume: this.intToBtc(offer.primaryMarketVolume), + payment_method: offer.paymentMethod, + offer_fee_txid: null, + }; + } + + private intToBtc(val: number): string { + return (val / 100000000).toFixed(8); + } +} + +export default new BisqMarketsApi(); diff --git a/backend/src/api/bisq/markets.ts b/backend/src/api/bisq/markets.ts new file mode 100644 index 000000000..9bac9dd45 --- /dev/null +++ b/backend/src/api/bisq/markets.ts @@ -0,0 +1,90 @@ +const config = require('../../../mempool-config.json'); +import * as fs from 'fs'; +import { OffsersData, TradesData, Currency } from './interfaces'; +import bisqMarket from './markets-api'; + +class Bisq { + private static MARKET_JSON_PATH = config.BSQ_MARKETS_DATA_PATH + '/btc_mainnet/db'; + private static MARKET_JSON_FILE_PATHS = { + cryptoCurrency: '/crypto_currency_list.json', + fiatCurrency: '/fiat_currency_list.json', + offers: '/offers_statistics.json', + trades: '/trade_statistics.json', + }; + + private subdirectoryWatcher: fs.FSWatcher | undefined; + + constructor() {} + + startBisqService(): void { + this.checkForBisqDataFolder(); + this.loadBisqDumpFile(); + this.startSubDirectoryWatcher(); + } + + private checkForBisqDataFolder() { + if (!fs.existsSync(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency)) { + console.log(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency + ` doesn't exist. Make sure Bisq is running and the config is correct before starting the server.`); + return process.exit(1); + } + } + + private startSubDirectoryWatcher() { + if (this.subdirectoryWatcher) { + this.subdirectoryWatcher.close(); + } + if (!fs.existsSync(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency)) { + console.log(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency + ` doesn't exist. Trying to restart sub directory watcher again in 3 minutes.`); + setTimeout(() => this.startSubDirectoryWatcher(), 180000); + return; + } + let fsWait: NodeJS.Timeout | null = null; + this.subdirectoryWatcher = fs.watch(Bisq.MARKET_JSON_PATH, () => { + if (fsWait) { + clearTimeout(fsWait); + } + fsWait = setTimeout(() => { + console.log(`Change detected in the Bisq market data folder.`); + this.loadBisqDumpFile(); + }, 2000); + }); + } + + private async loadBisqDumpFile(): Promise { + const start = new Date().getTime(); + console.log('Processing Bisq market data...'); + try { + const cryptoCurrencyData = await this.loadData(Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency); + const fiatCurrencyData = await this.loadData(Bisq.MARKET_JSON_FILE_PATHS.fiatCurrency); + const offersData = await this.loadData(Bisq.MARKET_JSON_FILE_PATHS.offers); + const tradesData = await this.loadData(Bisq.MARKET_JSON_FILE_PATHS.trades); + + bisqMarket.setData(cryptoCurrencyData, fiatCurrencyData, offersData, tradesData); + const time = new Date().getTime() - start; + console.log('Bisq market data processed in ' + time + ' ms'); + } catch (e) { + console.log('loadBisqMarketDataDumpFile() error.', e.message); + } + } + + private loadData(path: string): Promise { + return new Promise((resolve, reject) => { + if (!fs.existsSync(Bisq.MARKET_JSON_PATH + path)) { + return reject(path + ` doesn't exist`); + } + fs.readFile(Bisq.MARKET_JSON_PATH + path, 'utf8', (err, data) => { + if (err) { + reject(err); + } + try { + const parsedData = JSON.parse(data); + resolve(parsedData); + } catch (e) { + reject('JSON parse error (' + path + ')'); + } + }); + }); + } +} + +export default new Bisq(); diff --git a/backend/src/index.ts b/backend/src/index.ts index f5894f6c0..6abb69332 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -15,7 +15,8 @@ import diskCache from './api/disk-cache'; import statistics from './api/statistics'; import websocketHandler from './api/websocket-handler'; import fiatConversion from './api/fiat-conversion'; -import bisq from './api/bisq'; +import bisq from './api/bisq/bisq'; +import bisqMarkets from './api/bisq/markets'; class Server { wss: WebSocket.Server; @@ -61,6 +62,10 @@ class Server { bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price)); } + if (config.BISQ_MARKET_ENABLED) { + bisqMarkets.startBisqService(); + } + this.server.listen(config.HTTP_PORT, () => { console.log(`Server started on port ${config.HTTP_PORT}`); }); @@ -107,6 +112,19 @@ class Server { .get(config.API_ENDPOINT + 'bisq/txs/:index/:length', routes.getBisqTransactions) ; } + + if (config.BISQ_MARKET_ENABLED) { + this.app + .get(config.API_ENDPOINT + 'bisq/markets/currencies', routes.getBisqMarketCurrencies.bind(routes)) + .get(config.API_ENDPOINT + 'bisq/markets/depth', routes.getBisqMarketDepth.bind(routes)) + .get(config.API_ENDPOINT + 'bisq/markets/hloc', routes.getBisqMarketHloc.bind(routes)) + .get(config.API_ENDPOINT + 'bisq/markets/markets', routes.getBisqMarketMarkets.bind(routes)) + .get(config.API_ENDPOINT + 'bisq/markets/offers', routes.getBisqMarketOffers.bind(routes)) + .get(config.API_ENDPOINT + 'bisq/markets/ticker', routes.getBisqMarketTicker.bind(routes)) + .get(config.API_ENDPOINT + 'bisq/markets/trades', routes.getBisqMarketTrades.bind(routes)) + .get(config.API_ENDPOINT + 'bisq/markets/volumes', routes.getBisqMarketVolumes.bind(routes)) + ; + } } } diff --git a/backend/src/interfaces.ts b/backend/src/interfaces.ts index 9da3cde44..d652760ac 100644 --- a/backend/src/interfaces.ts +++ b/backend/src/interfaces.ts @@ -231,94 +231,9 @@ export interface VbytesPerSecond { vSize: number; } -export interface BisqBlocks { - chainHeight: number; - blocks: BisqBlock[]; -} +export interface RequiredSpec { [name: string]: RequiredParams; } -export interface BisqBlock { - height: number; - time: number; - hash: string; - previousBlockHash: string; - txs: BisqTransaction[]; -} - -export interface BisqTransaction { - txVersion: string; - id: string; - blockHeight: number; - blockHash: string; - time: number; - inputs: BisqInput[]; - outputs: BisqOutput[]; - txType: string; - txTypeDisplayString: string; - burntFee: number; - invalidatedBsq: number; - unlockBlockHeight: number; -} - -export interface BisqStats { - minted: number; - burnt: number; - addresses: number; - unspent_txos: number; - spent_txos: number; -} - -interface BisqInput { - spendingTxOutputIndex: number; - spendingTxId: string; - bsqAmount: number; - isVerified: boolean; - address: string; - time: number; -} - -interface BisqOutput { - txVersion: string; - txId: string; - index: number; - bsqAmount: number; - btcAmount: number; - height: number; - isVerified: boolean; - burntFee: number; - invalidatedBsq: number; - address: string; - scriptPubKey: BisqScriptPubKey; - time: any; - txType: string; - txTypeDisplayString: string; - txOutputType: string; - txOutputTypeDisplayString: string; - lockTime: number; - isUnspent: boolean; - spentInfo: SpentInfo; - opReturn?: string; -} - -interface BisqScriptPubKey { - addresses: string[]; - asm: string; - hex: string; - reqSigs: number; - type: string; -} - -interface SpentInfo { - height: number; - inputIndex: number; - txId: string; -} - -export interface BisqTrade { - direction: string; - price: string; - amount: string; - volume: string; - payment_method: string; - trade_id: string; - trade_date: number; +interface RequiredParams { + required: boolean; + types: ('@string' | '@number' | string)[]; } diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 3f34cc8da..4e2b06441 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -5,7 +5,10 @@ import feeApi from './api/fee-api'; import backendInfo from './api/backend-info'; import mempoolBlocks from './api/mempool-blocks'; import mempool from './api/mempool'; -import bisq from './api/bisq'; +import bisq from './api/bisq/bisq'; +import bisqMarket from './api/bisq/markets-api'; +import { RequiredSpec } from './interfaces'; +import { MarketsApiError } from './api/bisq/interfaces'; class Routes { private cache = {}; @@ -161,6 +164,258 @@ class Routes { res.status(404).send('Bisq address not found'); } } + + public getBisqMarketCurrencies(req: Request, res: Response) { + const constraints: RequiredSpec = { + 'type': { + required: false, + types: ['crypto', 'fiat', 'all'] + }, + }; + + const p = this.parseRequestParameters(req, constraints); + if (p.error) { + res.status(501).json(this.getBisqMarketErrorResponse(p.error)); + return; + } + + const result = bisqMarket.getCurrencies(p.type); + if (result) { + res.json(result); + } else { + res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketCurrencies error')); + } + } + + public getBisqMarketDepth(req: Request, res: Response) { + const constraints: RequiredSpec = { + 'market': { + required: true, + types: ['@string'] + }, + }; + + const p = this.parseRequestParameters(req, constraints); + if (p.error) { + res.status(501).json(this.getBisqMarketErrorResponse(p.error)); + return; + } + + const result = bisqMarket.getDepth(p.market); + if (result) { + res.json(result); + } else { + res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketDepth error')); + } + } + + public getBisqMarketMarkets(req: Request, res: Response) { + const result = bisqMarket.getMarkets(); + if (result) { + res.json(result); + } else { + res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketMarkets error')); + } + } + + public getBisqMarketTrades(req: Request, res: Response) { + const constraints: RequiredSpec = { + 'market': { + required: true, + types: ['@string'] + }, + 'timestamp_from': { + required: false, + types: ['@number'] + }, + 'timestamp_to': { + required: false, + types: ['@number'] + }, + 'trade_id_to': { + required: false, + types: ['@string'] + }, + 'trade_id_from': { + required: false, + types: ['@string'] + }, + 'direction': { + required: false, + types: ['buy', 'sell'] + }, + 'limit': { + required: false, + types: ['@number'] + }, + 'sort': { + required: false, + types: ['asc', 'desc'] + } + }; + + const p = this.parseRequestParameters(req, constraints); + if (p.error) { + res.status(501).json(this.getBisqMarketErrorResponse(p.error)); + return; + } + + const result = bisqMarket.getTrades(p.market, p.timestamp_from, + p.timestamp_to, p.trade_id_from, p.trade_id_to, p.direction, p.limit, p.sort); + if (result) { + res.json(result); + } else { + res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketTrades error')); + } + } + + public getBisqMarketOffers(req: Request, res: Response) { + const constraints: RequiredSpec = { + 'market': { + required: true, + types: ['@string'] + }, + 'direction': { + required: false, + types: ['BUY', 'SELL'] + }, + }; + + const p = this.parseRequestParameters(req, constraints); + if (p.error) { + res.status(501).json(this.getBisqMarketErrorResponse(p.error)); + return; + } + + const result = bisqMarket.getCurrencies(p.type); + if (result) { + res.json(result); + } else { + res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketOffers error')); + } + } + + private parseRequestParameters(req: Request, params: RequiredSpec): { [name: string]: any; } { + const final = {}; + for (const i in params) { + if (params.hasOwnProperty(i)) { + if (params[i].required && !req.query[i]) { + return { error: i + ' parameter missing'}; + } + if (typeof req.query[i] === 'string') { + if (params[i].types.indexOf('@number') > -1) { + const number = parseInt((req.query[i] || '0').toString(), 10); + final[i] = number; + } else if (params[i].types.indexOf('@string') > -1) { + final[i] = req.query[i]; + } else if (params[i].types.indexOf((req.query[i] || '').toString()) > -1) { + final[i] = req.query[i]; + } else { + return { error: i + ' parameter invalid'}; + } + } + } + } + return final; + } + + public getBisqMarketVolumes(req: Request, res: Response) { + const constraints: RequiredSpec = { + 'market': { + required: true, + types: ['@string'] + }, + 'interval': { + required: false, + types: ['minute', 'half_hour', 'hour', 'half_day', 'day', 'week', 'month', 'year', 'auto'] + }, + 'timestamp_from': { + required: false, + types: ['@number'] + }, + 'timestamp_to': { + required: false, + types: ['@number'] + }, + }; + + const p = this.parseRequestParameters(req, constraints); + if (p.error) { + res.status(501).json(this.getBisqMarketErrorResponse(p.error)); + return; + } + + const result = bisqMarket.getVolumes(p.timestamp_from, p.timestamp_to, p.interval, p.market); + if (result) { + res.json(result); + } else { + res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketVolumes error')); + } + } + + public getBisqMarketHloc(req: Request, res: Response) { + const constraints: RequiredSpec = { + 'market': { + required: true, + types: ['@string'] + }, + 'interval': { + required: false, + types: ['minute', 'half_hour', 'hour', 'half_day', 'day', 'week', 'month', 'year', 'auto'] + }, + 'timestamp_from': { + required: false, + types: ['@number'] + }, + 'timestamp_to': { + required: false, + types: ['@number'] + }, + }; + + const p = this.parseRequestParameters(req, constraints); + if (p.error) { + res.status(501).json(this.getBisqMarketErrorResponse(p.error)); + return; + } + + const result = bisqMarket.getHloc(p.market, p.interval, p.timestamp_from, p.timestamp_to); + if (result) { + res.json(result); + } else { + res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketHloc error')); + } + } + + public getBisqMarketTicker(req: Request, res: Response) { + const constraints: RequiredSpec = { + 'market': { + required: false, + types: ['@string'] + }, + }; + + const p = this.parseRequestParameters(req, constraints); + if (p.error) { + res.status(501).json(this.getBisqMarketErrorResponse(p.error)); + return; + } + + const result = bisqMarket.getCurrencies(p.market); + if (result) { + res.json(result); + } else { + res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketTicker error')); + } + } + + private getBisqMarketErrorResponse(message: string): MarketsApiError { + return { + 'success': 0, + 'error': message + }; + } + } export default new Routes(); From 98cc81c53d2e4a2690ede968b94272f75933a16c Mon Sep 17 00:00:00 2001 From: softsimon Date: Sat, 12 Sep 2020 01:26:51 +0700 Subject: [PATCH 2/4] Corrections to parameters handling and sorting. --- backend/src/api/bisq/interfaces.ts | 4 +- backend/src/api/bisq/markets-api.ts | 76 +++++++++++++++++++---------- backend/src/routes.ts | 53 ++++++++++---------- 3 files changed, 80 insertions(+), 53 deletions(-) diff --git a/backend/src/api/bisq/interfaces.ts b/backend/src/api/bisq/interfaces.ts index 500789ab9..c35800e40 100644 --- a/backend/src/api/bisq/interfaces.ts +++ b/backend/src/api/bisq/interfaces.ts @@ -89,6 +89,7 @@ export interface BisqTrade { payment_method: string; trade_id: string; trade_date: number; + market?: string; } export interface Currencies { [txid: string]: Currency; } @@ -97,7 +98,8 @@ export interface Currency { code: string; name: string; precision: number; - type: string; + + _type: string; } export interface Depth { [market: string]: Market; } diff --git a/backend/src/api/bisq/markets-api.ts b/backend/src/api/bisq/markets-api.ts index 83d3e9b76..c209c4930 100644 --- a/backend/src/api/bisq/markets-api.ts +++ b/backend/src/api/bisq/markets-api.ts @@ -15,8 +15,8 @@ class BisqMarketsApi { this.offersData = offers; this.tradesData = trades; - this.fiatCurrencyData.forEach((currency) => currency.type = 'fiat'); - this.cryptoCurrencyData.forEach((currency) => currency.type = 'crypto'); + this.fiatCurrencyData.forEach((currency) => currency._type = 'fiat'); + this.cryptoCurrencyData.forEach((currency) => currency._type = 'crypto'); this.tradesData.forEach((trade) => { trade._market = trade.currencyPair.toLowerCase().replace('/', '_'); }); @@ -51,13 +51,13 @@ class BisqMarketsApi { const currencyPair = market.replace('_', '/').toUpperCase(); const buys = this.offersData - .filter((offer) => offer.currencyPair === currencyPair && offer.direction === 'BUY') + .filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'BUY') .map((offer) => offer.price) .sort((a, b) => b - a) .map((price) => this.intToBtc(price)); const sells = this.offersData - .filter((offer) => offer.currencyPair === currencyPair && offer.direction === 'SELL') + .filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'SELL') .map((offer) => offer.price) .sort((a, b) => a - b) .map((price) => this.intToBtc(price)); @@ -72,22 +72,24 @@ class BisqMarketsApi { getOffers( market: string, - direction?: 'BUY' | 'SELL', + direction?: 'buy' | 'sell', ): Offers { const currencyPair = market.replace('_', '/').toUpperCase(); let buys: Offer[] | null = null; let sells: Offer[] | null = null; - if (!direction || direction === 'BUY') { + if (!direction || direction === 'buy') { buys = this.offersData - .filter((offer) => offer.currencyPair === currencyPair && offer.direction === 'BUY') + .filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'BUY') + .sort((a, b) => b.price - a.price) .map((offer) => this.offerDataToOffer(offer)); } - if (!direction || direction === 'SELL') { + if (!direction || direction === 'sell') { sells = this.offersData - .filter((offer) => offer.currencyPair === currencyPair && offer.direction === 'SELL') + .filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'SELL') + .sort((a, b) => a.price - b.price) .map((offer) => this.offerDataToOffer(offer)); } @@ -108,14 +110,14 @@ class BisqMarketsApi { continue; } - const isFiat = allCurrencies[currency].type === 'fiat'; + const isFiat = allCurrencies[currency]._type === 'fiat'; const pmarketname = allCurrencies['BTC']['name']; const lsymbol = isFiat ? 'BTC' : currency; const rsymbol = isFiat ? currency : 'BTC'; const lname = isFiat ? pmarketname : allCurrencies[currency].name; const rname = isFiat ? allCurrencies[currency].name : pmarketname; - const ltype = isFiat ? 'crypto' : allCurrencies[currency].type; + const ltype = isFiat ? 'crypto' : allCurrencies[currency]._type; const rtype = isFiat ? 'fiat' : 'crypto'; const lprecision = 8; const rprecision = isFiat ? 2 : 8; @@ -155,9 +157,13 @@ class BisqMarketsApi { if (!timestamp_from) { timestamp_from = new Date('2016-01-01').getTime(); + } else { + timestamp_from = timestamp_from * 1000; } if (!timestamp_to) { timestamp_to = new Date().getTime(); + } else { + timestamp_to = timestamp_to * 1000; } const allCurrencies = this.getCurrencies(); @@ -206,12 +212,16 @@ class BisqMarketsApi { const currencyLeft = allCurrencies[currencyPairs[0]]; const currencyRight = allCurrencies[currencyPairs[1]]; - trade.tradePrice = trade.primaryMarketTradePrice * Math.pow(10, 8 - currencyRight.precision); - trade.tradeAmount = trade.primaryMarketTradeAmount * Math.pow(10, 8 - currencyLeft.precision); + if (!currencyLeft || !currencyRight) { + continue; + } + + const tradePrice = trade.primaryMarketTradePrice * Math.pow(10, 8 - currencyRight.precision); + const tradeAmount = trade.primaryMarketTradeAmount * Math.pow(10, 8 - currencyLeft.precision); const tradeVolume = trade.primaryMarketTradeVolume * Math.pow(10, 8 - currencyRight.precision); - trade._tradePrice = this.intToBtc(trade.tradePrice); - trade._tradeAmount = this.intToBtc(trade.tradeAmount); + trade._tradePrice = this.intToBtc(tradePrice); + trade._tradeAmount = this.intToBtc(tradeAmount); trade._tradeVolume = this.intToBtc(tradeVolume); trade._offerAmount = this.intToBtc(trade.offerAmount); @@ -226,17 +236,27 @@ class BisqMarketsApi { matches = []; } - return matches.map((trade) => { - return { - direction: trade.direction, - price: trade._tradePrice, - amount: trade._tradeAmount, - volume: trade._tradeVolume, - payment_method: trade.paymentMethod, - trade_id: trade.offerId, - trade_date: trade.tradeDate, - }; - }); + if (sort === 'asc') { + matches.sort((a, b) => a.tradeDate - b.tradeDate); + } else { + matches.sort((a, b) => b.tradeDate - a.tradeDate); + } + + return matches.map((trade) => { + const bsqTrade: BisqTrade = { + direction: trade.primaryMarketDirection, + price: trade._tradePrice, + amount: trade._tradeAmount, + volume: trade._tradeVolume, + payment_method: trade.paymentMethod, + trade_id: trade.offerId, + trade_date: trade.tradeDate, + }; + if (market === 'all') { + bsqTrade.market = trade._market; + } + return bsqTrade; + }); } getVolumes( @@ -262,9 +282,13 @@ class BisqMarketsApi { ): HighLowOpenClose[] { if (!timestamp_from) { timestamp_from = new Date('2016-01-01').getTime(); + } else { + timestamp_from = timestamp_from * 1000; } if (!timestamp_to) { timestamp_to = new Date().getTime(); + } else { + timestamp_to = timestamp_to * 1000; } return []; } diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 4e2b06441..115354aca 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -277,7 +277,7 @@ class Routes { }, 'direction': { required: false, - types: ['BUY', 'SELL'] + types: ['buy', 'sell'] }, }; @@ -287,7 +287,7 @@ class Routes { return; } - const result = bisqMarket.getCurrencies(p.type); + const result = bisqMarket.getOffers(p.market, p.direction); if (result) { res.json(result); } else { @@ -295,30 +295,6 @@ class Routes { } } - private parseRequestParameters(req: Request, params: RequiredSpec): { [name: string]: any; } { - const final = {}; - for (const i in params) { - if (params.hasOwnProperty(i)) { - if (params[i].required && !req.query[i]) { - return { error: i + ' parameter missing'}; - } - if (typeof req.query[i] === 'string') { - if (params[i].types.indexOf('@number') > -1) { - const number = parseInt((req.query[i] || '0').toString(), 10); - final[i] = number; - } else if (params[i].types.indexOf('@string') > -1) { - final[i] = req.query[i]; - } else if (params[i].types.indexOf((req.query[i] || '').toString()) > -1) { - final[i] = req.query[i]; - } else { - return { error: i + ' parameter invalid'}; - } - } - } - } - return final; - } - public getBisqMarketVolumes(req: Request, res: Response) { const constraints: RequiredSpec = { 'market': { @@ -409,6 +385,31 @@ class Routes { } } + private parseRequestParameters(req: Request, params: RequiredSpec): { [name: string]: any; } { + const final = {}; + for (const i in params) { + if (params.hasOwnProperty(i)) { + if (params[i].required && !req.query[i]) { + return { error: i + ' parameter missing'}; + } + if (typeof req.query[i] === 'string') { + const str = (req.query[i] || '').toString().toLowerCase(); + if (params[i].types.indexOf('@number') > -1) { + const number = parseInt((str).toString(), 10); + final[i] = number; + } else if (params[i].types.indexOf('@string') > -1) { + final[i] = str; + } else if (params[i].types.indexOf(str) > -1) { + final[i] = str; + } else { + return { error: i + ' parameter invalid'}; + } + } + } + } + return final; + } + private getBisqMarketErrorResponse(message: string): MarketsApiError { return { 'success': 0, From bafe2db09475fece30e91f83c9b326a47ad5cf36 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 13 Sep 2020 17:51:53 +0700 Subject: [PATCH 3/4] HLOC markets api. --- backend/mempool-config.sample.json | 2 +- backend/package-lock.json | 18 ++ backend/package.json | 2 + backend/src/api/bisq/interfaces.ts | 12 +- backend/src/api/bisq/markets-api.ts | 330 ++++++++++++++++++++-------- backend/src/api/bisq/markets.ts | 6 +- backend/src/interfaces.ts | 2 +- backend/src/routes.ts | 8 +- 8 files changed, 282 insertions(+), 98 deletions(-) diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json index 9d8e4af37..190fd7164 100644 --- a/backend/mempool-config.sample.json +++ b/backend/mempool-config.sample.json @@ -17,7 +17,7 @@ "BISQ_ENABLED": false, "BISQ_MARKET_ENABLED": false, "BSQ_BLOCKS_DATA_PATH": "/bisq/data", - "BSQ_MARKETS_DATA_PATH": "/bisq-markets/data", + "BSQ_MARKETS_DATA_PATH": "/bisq-folder/Bisq", "SSL": false, "SSL_CERT_FILE_PATH": "/etc/letsencrypt/live/mysite/fullchain.pem", "SSL_KEY_FILE_PATH": "/etc/letsencrypt/live/mysite/privkey.pem" diff --git a/backend/package-lock.json b/backend/package-lock.json index 6052b4260..02bbf97f7 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -86,6 +86,11 @@ "@types/range-parser": "*" } }, + "@types/locutus": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/locutus/-/locutus-0.0.6.tgz", + "integrity": "sha512-P+BQds4wrJhqKiIOBWAYpbsE9UOztnnqW9zHk4Bci7kCXjEQAA7FJrD9HX5JU2Z36fhE2WDctuuIpLvqDsciWQ==" + }, "@types/mime": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", @@ -446,6 +451,11 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -733,6 +743,14 @@ "verror": "1.10.0" } }, + "locutus": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/locutus/-/locutus-2.0.12.tgz", + "integrity": "sha512-wnzhY9xOdDb2djr17kQhTh9oZgEfp78zI27KRRiiV1GnPXWA2xfVODbpH3QgpIuUMLupM02+6X/rJXvktTpnoA==", + "requires": { + "es6-promise": "^4.2.5" + } + }, "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index 884a9fb90..1de591301 100644 --- a/backend/package.json +++ b/backend/package.json @@ -29,6 +29,7 @@ "express": "^4.17.1", "mysql2": "^1.6.1", "request": "^2.88.2", + "locutus": "^2.0.12", "ws": "^7.3.1" }, "devDependencies": { @@ -36,6 +37,7 @@ "@types/express": "^4.17.2", "@types/request": "^2.48.2", "@types/ws": "^6.0.4", + "@types/locutus": "^0.0.6", "tslint": "~6.1.0", "typescript": "~3.9.7" } diff --git a/backend/src/api/bisq/interfaces.ts b/backend/src/api/bisq/interfaces.ts index c35800e40..1eb890fc5 100644 --- a/backend/src/api/bisq/interfaces.ts +++ b/backend/src/api/bisq/interfaces.ts @@ -222,10 +222,14 @@ export interface TradesData { primaryMarketTradeVolume: number; _market: string; - _tradePrice: string; - _tradeAmount: string; - _tradeVolume: string; - _offerAmount: string; + _tradePriceStr: string; + _tradeAmountStr: string; + _tradeVolumeStr: string; + _offerAmountStr: string; + _tradePrice: number; + _tradeAmount: number; + _tradeVolume: number; + _offerAmount: number; } export interface MarketVolume { diff --git a/backend/src/api/bisq/markets-api.ts b/backend/src/api/bisq/markets-api.ts index c209c4930..910fc2d0b 100644 --- a/backend/src/api/bisq/markets-api.ts +++ b/backend/src/api/bisq/markets-api.ts @@ -1,6 +1,8 @@ import { Currencies, OffsersData, TradesData, Depth, Currency, Interval, HighLowOpenClose, Markets, Offers, Offer, BisqTrade, MarketVolume, Tickers } from './interfaces'; +import * as datetime from 'locutus/php/datetime'; + class BisqMarketsApi { private cryptoCurrencyData: Currency[] = []; private fiatCurrencyData: Currency[] = []; @@ -149,92 +151,18 @@ class BisqMarketsApi { direction?: 'buy' | 'sell', limit: number = 100, sort: 'asc' | 'desc' = 'desc', - ): BisqTrade[] { + ): BisqTrade[] { limit = Math.min(limit, 2000); - let trade_id_from_ts: number | null = null; - let trade_id_to_ts: number | null = null; const _market = market === 'all' ? null : market; if (!timestamp_from) { - timestamp_from = new Date('2016-01-01').getTime(); - } else { - timestamp_from = timestamp_from * 1000; + timestamp_from = new Date('2016-01-01').getTime() / 1000; } if (!timestamp_to) { - timestamp_to = new Date().getTime(); - } else { - timestamp_to = timestamp_to * 1000; + timestamp_to = new Date().getTime() / 1000; } - const allCurrencies = this.getCurrencies(); - - // note: the offer_id_from/to depends on iterating over trades in - // descending chronological order. - const tradesDataSorted = this.tradesData.slice(); - if (sort === 'asc') { - tradesDataSorted.reverse(); - } - - let matches: TradesData[] = []; - for (const trade of tradesDataSorted) { - if (trade_id_from === trade.offerId) { - trade_id_from_ts = trade.tradeDate; - } - if (trade_id_to === trade.offerId) { - trade_id_to_ts = trade.tradeDate; - } - if (trade_id_to && trade_id_to_ts === null) { - continue; - } - if (trade_id_from && trade_id_from_ts != null && trade_id_from_ts !== trade.tradeDate ) { - continue; - } - if (_market && _market !== trade._market) { - continue; - } - if (timestamp_from && timestamp_from > trade.tradeDate) { - continue; - } - if (timestamp_to && timestamp_to < trade.tradeDate) { - continue; - } - if (direction && direction !== trade.direction.toLowerCase() ) { - continue; - } - - // Filter out bogus trades with BTC/BTC or XXX/XXX market. - // See github issue: https://github.com/bitsquare/bitsquare/issues/883 - const currencyPairs = trade.currencyPair.split('/'); - if (currencyPairs[0] === currencyPairs[1]) { - continue; - } - - const currencyLeft = allCurrencies[currencyPairs[0]]; - const currencyRight = allCurrencies[currencyPairs[1]]; - - if (!currencyLeft || !currencyRight) { - continue; - } - - const tradePrice = trade.primaryMarketTradePrice * Math.pow(10, 8 - currencyRight.precision); - const tradeAmount = trade.primaryMarketTradeAmount * Math.pow(10, 8 - currencyLeft.precision); - const tradeVolume = trade.primaryMarketTradeVolume * Math.pow(10, 8 - currencyRight.precision); - - trade._tradePrice = this.intToBtc(tradePrice); - trade._tradeAmount = this.intToBtc(tradeAmount); - trade._tradeVolume = this.intToBtc(tradeVolume); - trade._offerAmount = this.intToBtc(trade.offerAmount); - - matches.push(trade); - - if (matches.length >= limit) { - break; - } - } - - if ((trade_id_from && !trade_id_from_ts) || (trade_id_to && !trade_id_to_ts) ) { - matches = []; - } + const matches = this.getTradesByCriteria(_market, timestamp_to, timestamp_from, trade_id_to, trade_id_from, direction, sort, limit); if (sort === 'asc') { matches.sort((a, b) => a.tradeDate - b.tradeDate); @@ -245,9 +173,9 @@ class BisqMarketsApi { return matches.map((trade) => { const bsqTrade: BisqTrade = { direction: trade.primaryMarketDirection, - price: trade._tradePrice, - amount: trade._tradeAmount, - volume: trade._tradeVolume, + price: trade._tradePriceStr, + amount: trade._tradeAmountStr, + volume: trade._tradeVolumeStr, payment_method: trade.paymentMethod, trade_id: trade.offerId, trade_date: trade.tradeDate, @@ -279,20 +207,246 @@ class BisqMarketsApi { interval: Interval = 'auto', timestamp_from?: number, timestamp_to?: number, + milliseconds?: boolean, ): HighLowOpenClose[] { + if (milliseconds) { + timestamp_from = timestamp_from ? timestamp_from / 1000 : timestamp_from; + timestamp_to = timestamp_to ? timestamp_to / 1000 : timestamp_to; + } if (!timestamp_from) { - timestamp_from = new Date('2016-01-01').getTime(); - } else { - timestamp_from = timestamp_from * 1000; + timestamp_from = new Date('2016-01-01').getTime() / 1000; } if (!timestamp_to) { - timestamp_to = new Date().getTime(); - } else { - timestamp_to = timestamp_to * 1000; + timestamp_to = new Date().getTime() / 1000; } - return []; + + const range = timestamp_to - timestamp_from; + + const trades = this.getTradesByCriteria(market, timestamp_to, timestamp_from, + undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER); + + if (interval === 'auto') { + // two days range loads minute data + if (range <= 3600) { + // up to one hour range loads minutely data + interval = 'minute'; + } else if (range <= 1 * 24 * 3600) { + // up to one day range loads half-hourly data + interval = 'half_hour'; + } else if (range <= 3 * 24 * 3600) { + // up to 3 day range loads hourly data + interval = 'hour'; + } else if (range <= 7 * 24 * 3600) { + // up to 7 day range loads half-daily data + interval = 'half_day'; + } else if (range <= 60 * 24 * 3600) { + // up to 2 month range loads daily data + interval = 'day'; + } else if (range <= 12 * 31 * 24 * 3600) { + // up to one year range loads weekly data + interval = 'week'; + } else if (range <= 12 * 31 * 24 * 3600) { + // up to 5 year range loads monthly data + interval = 'month'; + } else { + // greater range loads yearly data + interval = 'year'; + } + } + + const hlocs = this.getTradesSummarized(trades, timestamp_from, interval); + + return hlocs; } + private getTradesSummarized(trades: TradesData[], timestamp_from, interval: string): HighLowOpenClose[] { + const intervals: any = {}; + const intervals_prices: any = {}; + const one_period = false; + + for (const trade of trades) { + const traded_at = trade.tradeDate / 1000; + const interval_start = one_period ? timestamp_from : this.intervalStart(traded_at, interval); + + if (!intervals[interval_start]) { + intervals[interval_start] = { + 'open': 0, + 'close': 0, + 'high': 0, + 'low': 0, + 'avg': 0, + 'volume_right': 0, + 'volume_left': 0, + }; + intervals_prices[interval_start] = []; + } + const period = intervals[interval_start]; + const price = trade._tradePrice; + + if (!intervals_prices[interval_start]['leftvol']) { + intervals_prices[interval_start]['leftvol'] = []; + } + if (!intervals_prices[interval_start]['rightvol']) { + intervals_prices[interval_start]['rightvol'] = []; + } + + intervals_prices[interval_start]['leftvol'].push(trade._tradeAmount); + intervals_prices[interval_start]['rightvol'].push(trade._tradeVolume); + + if (price) { + const plow = period['low']; + period['period_start'] = interval_start; + period['open'] = period['open'] || price; + period['close'] = price; + period['high'] = price > period['high'] ? price : period['high']; + period['low'] = (plow && price > plow) ? period['low'] : price; + period['avg'] = intervals_prices[interval_start]['rightvol'].reduce((p: number, c: number) => c + p, 0) + / intervals_prices[interval_start]['leftvol'].reduce((c: number, p: number) => c + p, 0) * 100000000; + period['volume_left'] += trade._tradeAmount; + period['volume_right'] += trade._tradeVolume; + } + } + + const hloc: HighLowOpenClose[] = []; + + for (const p in intervals) { + if (intervals.hasOwnProperty(p)) { + const period = intervals[p]; + hloc.push({ + period_start: period['period_start'], + open: this.intToBtc(period['open']), + close: this.intToBtc(period['close']), + high: this.intToBtc(period['high']), + low: this.intToBtc(period['low']), + avg: this.intToBtc(period['avg']), + volume_right: this.intToBtc(period['volume_right']), + volume_left: this.intToBtc(period['volume_left']), + }); + } + } + + return hloc; + } + + private getTradesByCriteria( + market: string | null, + timestamp_to: number, + timestamp_from: number, + trade_id_to: string | undefined, + trade_id_from: string | undefined, + direction: 'buy' | 'sell' | undefined, + sort: string, limit: number, + integerAmounts: boolean = true, + ): TradesData[] { + let trade_id_from_ts: number | null = null; + let trade_id_to_ts: number | null = null; + const allCurrencies = this.getCurrencies(); + + const timestampFromMilli = timestamp_from * 1000; + const timestampToMilli = timestamp_to * 1000; + + // note: the offer_id_from/to depends on iterating over trades in + // descending chronological order. + const tradesDataSorted = this.tradesData.slice(); + if (sort === 'asc') { + tradesDataSorted.reverse(); + } + + let matches: TradesData[] = []; + for (const trade of tradesDataSorted) { + if (trade_id_from === trade.offerId) { + trade_id_from_ts = trade.tradeDate; + } + if (trade_id_to === trade.offerId) { + trade_id_to_ts = trade.tradeDate; + } + if (trade_id_to && trade_id_to_ts === null) { + continue; + } + if (trade_id_from && trade_id_from_ts != null && trade_id_from_ts !== trade.tradeDate) { + continue; + } + if (market && market !== trade._market) { + continue; + } + if (timestampFromMilli && timestampFromMilli > trade.tradeDate) { + continue; + } + if (timestampToMilli && timestampToMilli < trade.tradeDate) { + continue; + } + if (direction && direction !== trade.direction.toLowerCase()) { + continue; + } + + // Filter out bogus trades with BTC/BTC or XXX/XXX market. + // See github issue: https://github.com/bitsquare/bitsquare/issues/883 + const currencyPairs = trade.currencyPair.split('/'); + if (currencyPairs[0] === currencyPairs[1]) { + continue; + } + + const currencyLeft = allCurrencies[currencyPairs[0]]; + const currencyRight = allCurrencies[currencyPairs[1]]; + + if (!currencyLeft || !currencyRight) { + continue; + } + + const tradePrice = trade.primaryMarketTradePrice * Math.pow(10, 8 - currencyRight.precision); + const tradeAmount = trade.primaryMarketTradeAmount * Math.pow(10, 8 - currencyLeft.precision); + const tradeVolume = trade.primaryMarketTradeVolume * Math.pow(10, 8 - currencyRight.precision); + + if (integerAmounts) { + trade._tradePrice = tradePrice; + trade._tradeAmount = tradeAmount; + trade._tradeVolume = tradeVolume; + trade._offerAmount = trade.offerAmount; + } else { + trade._tradePriceStr = this.intToBtc(tradePrice); + trade._tradeAmountStr = this.intToBtc(tradeAmount); + trade._tradeVolumeStr = this.intToBtc(tradeVolume); + trade._offerAmountStr = this.intToBtc(trade.offerAmount); + } + + matches.push(trade); + + if (matches.length >= limit) { + break; + } + } + + if ((trade_id_from && !trade_id_from_ts) || (trade_id_to && !trade_id_to_ts)) { + matches = []; + } + return matches; + } + + private intervalStart(ts: number, interval: string) { + switch (interval) { + case 'minute': + return (ts - (ts % 60)); + case '10_minute': + return (ts - (ts % 600)); + case 'half_hour': + return (ts - (ts % 1800)); + case 'hour': + return (ts - (ts % 3600)); + case 'half_day': + return (ts - (ts % (3600 * 12))); + case 'day': + return datetime.strtotime('midnight today', ts); + case 'week': + return datetime.strtotime('midnight sunday last week', ts); + case 'month': + return datetime.strtotime('midnight first day of this month', ts); + case 'year': + return datetime.strtotime('midnight first day of january', ts); + default: + throw new Error('Unsupported interval: ' + interval); + } +} + private offerDataToOffer(offer: OffsersData): Offer { return { offer_id: offer.id, diff --git a/backend/src/api/bisq/markets.ts b/backend/src/api/bisq/markets.ts index 9bac9dd45..d9902a00b 100644 --- a/backend/src/api/bisq/markets.ts +++ b/backend/src/api/bisq/markets.ts @@ -19,7 +19,7 @@ class Bisq { startBisqService(): void { this.checkForBisqDataFolder(); this.loadBisqDumpFile(); - this.startSubDirectoryWatcher(); + this.startBisqDirectoryWatcher(); } private checkForBisqDataFolder() { @@ -29,13 +29,13 @@ class Bisq { } } - private startSubDirectoryWatcher() { + private startBisqDirectoryWatcher() { if (this.subdirectoryWatcher) { this.subdirectoryWatcher.close(); } if (!fs.existsSync(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency)) { console.log(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency + ` doesn't exist. Trying to restart sub directory watcher again in 3 minutes.`); - setTimeout(() => this.startSubDirectoryWatcher(), 180000); + setTimeout(() => this.startBisqDirectoryWatcher(), 180000); return; } let fsWait: NodeJS.Timeout | null = null; diff --git a/backend/src/interfaces.ts b/backend/src/interfaces.ts index d652760ac..019168ae5 100644 --- a/backend/src/interfaces.ts +++ b/backend/src/interfaces.ts @@ -235,5 +235,5 @@ export interface RequiredSpec { [name: string]: RequiredParams; } interface RequiredParams { required: boolean; - types: ('@string' | '@number' | string)[]; + types: ('@string' | '@number' | '@boolean' | string)[]; } diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 115354aca..0aaea5212 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -347,6 +347,10 @@ class Routes { required: false, types: ['@number'] }, + 'milliseconds': { + required: false, + types: ['@boolean'] + }, }; const p = this.parseRequestParameters(req, constraints); @@ -355,7 +359,7 @@ class Routes { return; } - const result = bisqMarket.getHloc(p.market, p.interval, p.timestamp_from, p.timestamp_to); + const result = bisqMarket.getHloc(p.market, p.interval, p.timestamp_from, p.timestamp_to, p.milliseconds); if (result) { res.json(result); } else { @@ -399,6 +403,8 @@ class Routes { final[i] = number; } else if (params[i].types.indexOf('@string') > -1) { final[i] = str; + } else if (params[i].types.indexOf('@boolean') > -1) { + final[i] = str === 'true' || str === 'yes'; } else if (params[i].types.indexOf(str) > -1) { final[i] = str; } else { From a11135f358d5eb2094e39deabbc8eda5843098d7 Mon Sep 17 00:00:00 2001 From: softsimon Date: Tue, 15 Sep 2020 03:11:52 +0700 Subject: [PATCH 4/4] * /volumes API * /ticker API --- backend/mempool-config.sample.json | 4 +- backend/src/api/bisq/bisq.ts | 16 +- backend/src/api/bisq/interfaces.ts | 14 +- backend/src/api/bisq/markets-api.ts | 272 +++++++++++++++++++++------- backend/src/api/bisq/markets.ts | 2 +- backend/src/routes.ts | 24 ++- production/mempool-config.bisq.json | 2 +- 7 files changed, 246 insertions(+), 88 deletions(-) diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json index 190fd7164..9f7b773fb 100644 --- a/backend/mempool-config.sample.json +++ b/backend/mempool-config.sample.json @@ -15,9 +15,9 @@ "TX_PER_SECOND_SPAN_SECONDS": 150, "ELECTRS_API_URL": "http://localhost:50001", "BISQ_ENABLED": false, + "BISQ_BLOCKS_DATA_PATH": "/bisq/seednode-data/btc_mainnet/db/json", "BISQ_MARKET_ENABLED": false, - "BSQ_BLOCKS_DATA_PATH": "/bisq/data", - "BSQ_MARKETS_DATA_PATH": "/bisq-folder/Bisq", + "BISQ_MARKETS_DATA_PATH": "/bisq/seednode-data/btc_mainnet/db", "SSL": false, "SSL_CERT_FILE_PATH": "/etc/letsencrypt/live/mysite/fullchain.pem", "SSL_KEY_FILE_PATH": "/etc/letsencrypt/live/mysite/privkey.pem" diff --git a/backend/src/api/bisq/bisq.ts b/backend/src/api/bisq/bisq.ts index 0f4cdee81..5c5101890 100644 --- a/backend/src/api/bisq/bisq.ts +++ b/backend/src/api/bisq/bisq.ts @@ -72,8 +72,8 @@ class Bisq { } private checkForBisqDataFolder() { - if (!fs.existsSync(config.BSQ_BLOCKS_DATA_PATH + Bisq.BLOCKS_JSON_FILE_PATH)) { - console.log(config.BSQ_BLOCKS_DATA_PATH + Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist. Make sure Bisq is running and the config is correct before starting the server.`); + if (!fs.existsSync(config.BISQ_BLOCKS_DATA_PATH + Bisq.BLOCKS_JSON_FILE_PATH)) { + console.log(config.BISQ_BLOCKS_DATA_PATH + Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist. Make sure Bisq is running and the config is correct before starting the server.`); return process.exit(1); } } @@ -83,7 +83,7 @@ class Bisq { this.topDirectoryWatcher.close(); } let fsWait: NodeJS.Timeout | null = null; - this.topDirectoryWatcher = fs.watch(config.BSQ_BLOCKS_DATA_PATH, () => { + this.topDirectoryWatcher = fs.watch(config.BISQ_BLOCKS_DATA_PATH, () => { if (fsWait) { clearTimeout(fsWait); } @@ -105,13 +105,13 @@ class Bisq { if (this.subdirectoryWatcher) { this.subdirectoryWatcher.close(); } - if (!fs.existsSync(config.BSQ_BLOCKS_DATA_PATH + Bisq.BLOCKS_JSON_FILE_PATH)) { - console.log(config.BSQ_BLOCKS_DATA_PATH + Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist. Trying to restart sub directory watcher again in 3 minutes.`); + if (!fs.existsSync(config.BISQ_BLOCKS_DATA_PATH + Bisq.BLOCKS_JSON_FILE_PATH)) { + console.log(config.BISQ_BLOCKS_DATA_PATH + Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist. Trying to restart sub directory watcher again in 3 minutes.`); setTimeout(() => this.startSubDirectoryWatcher(), 180000); return; } let fsWait: NodeJS.Timeout | null = null; - this.subdirectoryWatcher = fs.watch(config.BSQ_BLOCKS_DATA_PATH + '/all', () => { + this.subdirectoryWatcher = fs.watch(config.BISQ_BLOCKS_DATA_PATH + '/all', () => { if (fsWait) { clearTimeout(fsWait); } @@ -243,10 +243,10 @@ class Bisq { private loadData(): Promise { return new Promise((resolve, reject) => { - if (!fs.existsSync(config.BSQ_BLOCKS_DATA_PATH + Bisq.BLOCKS_JSON_FILE_PATH)) { + if (!fs.existsSync(config.BISQ_BLOCKS_DATA_PATH + Bisq.BLOCKS_JSON_FILE_PATH)) { return reject(Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist`); } - fs.readFile(config.BSQ_BLOCKS_DATA_PATH + Bisq.BLOCKS_JSON_FILE_PATH, 'utf8', (err, data) => { + fs.readFile(config.BISQ_BLOCKS_DATA_PATH + Bisq.BLOCKS_JSON_FILE_PATH, 'utf8', (err, data) => { if (err) { reject(err); } diff --git a/backend/src/api/bisq/interfaces.ts b/backend/src/api/bisq/interfaces.ts index 1eb890fc5..f7cfaba43 100644 --- a/backend/src/api/bisq/interfaces.ts +++ b/backend/src/api/bisq/interfaces.ts @@ -234,8 +234,8 @@ export interface TradesData { export interface MarketVolume { period_start: number; - volume: string; num_trades: number; + volume: string; } export interface MarketsApiError { @@ -244,3 +244,15 @@ export interface MarketsApiError { } 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; +} diff --git a/backend/src/api/bisq/markets-api.ts b/backend/src/api/bisq/markets-api.ts index 910fc2d0b..64e027c02 100644 --- a/backend/src/api/bisq/markets-api.ts +++ b/backend/src/api/bisq/markets-api.ts @@ -1,5 +1,5 @@ import { Currencies, OffsersData, TradesData, Depth, Currency, Interval, HighLowOpenClose, - Markets, Offers, Offer, BisqTrade, MarketVolume, Tickers } from './interfaces'; + Markets, Offers, Offer, BisqTrade, MarketVolume, Tickers, Ticker, SummarizedIntervals, SummarizedInterval } from './interfaces'; import * as datetime from 'locutus/php/datetime'; @@ -8,6 +8,8 @@ class BisqMarketsApi { private fiatCurrencyData: Currency[] = []; private offersData: OffsersData[] = []; private tradesData: TradesData[] = []; + private fiatCurrenciesIndexed: { [code: string]: true } = {}; + private tradeDataByMarket: { [market: string]: TradesData[] } = {}; constructor() { } @@ -17,10 +19,18 @@ class BisqMarketsApi { this.offersData = offers; this.tradesData = trades; - this.fiatCurrencyData.forEach((currency) => currency._type = 'fiat'); + // Handle data for smarter memory caching + this.fiatCurrencyData.forEach((currency) => { + currency._type = 'fiat'; + this.fiatCurrenciesIndexed[currency.code] = true; + }); this.cryptoCurrencyData.forEach((currency) => currency._type = 'crypto'); this.tradesData.forEach((trade) => { trade._market = trade.currencyPair.toLowerCase().replace('/', '_'); + if (!this.tradeDataByMarket[trade._market]) { + this.tradeDataByMarket[trade._market] = []; + } + this.tradeDataByMarket[trade._market].push(trade); }); } @@ -153,7 +163,7 @@ class BisqMarketsApi { sort: 'asc' | 'desc' = 'desc', ): BisqTrade[] { limit = Math.min(limit, 2000); - const _market = market === 'all' ? null : market; + const _market = market === 'all' ? undefined : market; if (!timestamp_from) { timestamp_from = new Date('2016-01-01').getTime() / 1000; @@ -188,18 +198,149 @@ class BisqMarketsApi { } getVolumes( - timestamp_from: number, - timestamp_to: number, - interval: Interval, market?: string, - ): MarketVolume[] { - return []; + timestamp_from?: number, + timestamp_to?: number, + interval: Interval = 'auto', + milliseconds?: boolean, + ): MarketVolume[] { + if (milliseconds) { + timestamp_from = timestamp_from ? timestamp_from / 1000 : timestamp_from; + timestamp_to = timestamp_to ? timestamp_to / 1000 : timestamp_to; + } + if (!timestamp_from) { + timestamp_from = new Date('2016-01-01').getTime() / 1000; + } + if (!timestamp_to) { + timestamp_to = new Date().getTime() / 1000; + } + + const trades = this.getTradesByCriteria(market, timestamp_to, timestamp_from, + undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER); + + if (interval === 'auto') { + const range = timestamp_to - timestamp_from; + interval = this.getIntervalFromRange(range); + } + + const intervals: any = {}; + const marketVolumes: MarketVolume[] = []; + + for (const trade of trades) { + const traded_at = trade['tradeDate'] / 1000; + const interval_start = this.intervalStart(traded_at, interval); + + if (!intervals[interval_start]) { + intervals[interval_start] = { + 'volume': 0, + 'num_trades': 0, + }; + } + + const period = intervals[interval_start]; + period['period_start'] = interval_start; + period['volume'] += this.fiatCurrenciesIndexed[trade.currency] ? trade._tradeAmount : trade._tradeVolume; + period['num_trades']++; + } + + for (const p in intervals) { + if (intervals.hasOwnProperty(p)) { + const period = intervals[p]; + marketVolumes.push({ + period_start: period['period_start'], + num_trades: period['num_trades'], + volume: this.intToBtc(period['volume']), + }); + } + } + + return intervals; } getTicker( market?: string, - ): Tickers { - return {}; + ): Tickers | Ticker | null { + if (market) { + return this.getTickerFromMarket(market); + } + + const allMarkets = this.getMarkets(); + const tickers = {}; + for (const m in allMarkets) { + if (allMarkets.hasOwnProperty(m)) { + tickers[allMarkets[m].pair] = this.getTickerFromMarket(allMarkets[m].pair); + } + } + + return tickers; + } + + getTickerFromMarket(market: string): Ticker | null { + let ticker: Ticker; + const timestamp_from = datetime.strtotime('-24 hour'); + const timestamp_to = new Date().getTime() / 1000; + const trades = this.getTradesByCriteria(market, timestamp_to, timestamp_from, + undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER); + + const periods: SummarizedInterval[] = Object.values(this.getTradesSummarized(trades, timestamp_from)); + + const allCurrencies = this.getCurrencies(); + const currencyRight = allCurrencies[market.split('_')[1].toUpperCase()]; + + if (periods[0]) { + ticker = { + 'last': this.intToBtc(periods[0].close), + 'high': this.intToBtc(periods[0].high), + 'low': this.intToBtc(periods[0].low), + 'volume_left': this.intToBtc(periods[0].volume_left), + 'volume_right': this.intToBtc(periods[0].volume_right), + 'buy': null, + 'sell': null, + }; + } else { + const lastTrade = this.tradeDataByMarket[market]; + if (!lastTrade) { + return null; + } + const tradePrice = lastTrade[0].primaryMarketTradePrice * Math.pow(10, 8 - currencyRight.precision); + + const lastTradePrice = this.intToBtc(tradePrice); + ticker = { + 'last': lastTradePrice, + 'high': lastTradePrice, + 'low': lastTradePrice, + 'volume_left': '0', + 'volume_right': '0', + 'buy': null, + 'sell': null, + }; + } + + const timestampFromMilli = timestamp_from * 1000; + const timestampToMilli = timestamp_to * 1000; + + const currencyPair = market.replace('_', '/').toUpperCase(); + const offersData = this.offersData.slice().sort((a, b) => a.price - b.price); + + const buy = offersData.find((offer) => offer.currencyPair === currencyPair + && offer.primaryMarketDirection === 'BUY' + && offer.date >= timestampFromMilli + && offer.date <= timestampToMilli + ); + const sell = offersData.find((offer) => offer.currencyPair === currencyPair + && offer.primaryMarketDirection === 'SELL' + && offer.date >= timestampFromMilli + && offer.date <= timestampToMilli + ); + + if (buy) { + ticker.buy = this.intToBtc(buy.primaryMarketPrice * Math.pow(10, 8 - currencyRight.precision)); + } + if (sell) { + ticker.sell = this.intToBtc(sell.primaryMarketPrice * Math.pow(10, 8 - currencyRight.precision)); + } + + return ticker; } getHloc( @@ -220,53 +361,73 @@ class BisqMarketsApi { timestamp_to = new Date().getTime() / 1000; } - const range = timestamp_to - timestamp_from; - const trades = this.getTradesByCriteria(market, timestamp_to, timestamp_from, undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER); if (interval === 'auto') { - // two days range loads minute data - if (range <= 3600) { - // up to one hour range loads minutely data - interval = 'minute'; - } else if (range <= 1 * 24 * 3600) { - // up to one day range loads half-hourly data - interval = 'half_hour'; - } else if (range <= 3 * 24 * 3600) { - // up to 3 day range loads hourly data - interval = 'hour'; - } else if (range <= 7 * 24 * 3600) { - // up to 7 day range loads half-daily data - interval = 'half_day'; - } else if (range <= 60 * 24 * 3600) { - // up to 2 month range loads daily data - interval = 'day'; - } else if (range <= 12 * 31 * 24 * 3600) { - // up to one year range loads weekly data - interval = 'week'; - } else if (range <= 12 * 31 * 24 * 3600) { - // up to 5 year range loads monthly data - interval = 'month'; - } else { - // greater range loads yearly data - interval = 'year'; - } + const range = timestamp_to - timestamp_from; + interval = this.getIntervalFromRange(range); } - const hlocs = this.getTradesSummarized(trades, timestamp_from, interval); + const intervals = this.getTradesSummarized(trades, timestamp_from, interval); - return hlocs; + const hloc: HighLowOpenClose[] = []; + + for (const p in intervals) { + if (intervals.hasOwnProperty(p)) { + const period = intervals[p]; + hloc.push({ + period_start: period['period_start'], + open: this.intToBtc(period['open']), + close: this.intToBtc(period['close']), + high: this.intToBtc(period['high']), + low: this.intToBtc(period['low']), + avg: this.intToBtc(period['avg']), + volume_right: this.intToBtc(period['volume_right']), + volume_left: this.intToBtc(period['volume_left']), + }); + } + } + + return hloc; } - private getTradesSummarized(trades: TradesData[], timestamp_from, interval: string): HighLowOpenClose[] { + private getIntervalFromRange(range: number): Interval { + // two days range loads minute data + if (range <= 3600) { + // up to one hour range loads minutely data + return 'minute'; + } else if (range <= 1 * 24 * 3600) { + // up to one day range loads half-hourly data + return 'half_hour'; + } else if (range <= 3 * 24 * 3600) { + // up to 3 day range loads hourly data + return 'hour'; + } else if (range <= 7 * 24 * 3600) { + // up to 7 day range loads half-daily data + return 'half_day'; + } else if (range <= 60 * 24 * 3600) { + // up to 2 month range loads daily data + return 'day'; + } else if (range <= 12 * 31 * 24 * 3600) { + // up to one year range loads weekly data + return 'week'; + } else if (range <= 12 * 31 * 24 * 3600) { + // up to 5 year range loads monthly data + return 'month'; + } else { + // greater range loads yearly data + return 'year'; + } + } + + private getTradesSummarized(trades: TradesData[], timestamp_from: number, interval?: string): SummarizedIntervals { const intervals: any = {}; const intervals_prices: any = {}; - const one_period = false; for (const trade of trades) { const traded_at = trade.tradeDate / 1000; - const interval_start = one_period ? timestamp_from : this.intervalStart(traded_at, interval); + const interval_start = !interval ? timestamp_from : this.intervalStart(traded_at, interval); if (!intervals[interval_start]) { intervals[interval_start] = { @@ -306,30 +467,11 @@ class BisqMarketsApi { period['volume_right'] += trade._tradeVolume; } } - - const hloc: HighLowOpenClose[] = []; - - for (const p in intervals) { - if (intervals.hasOwnProperty(p)) { - const period = intervals[p]; - hloc.push({ - period_start: period['period_start'], - open: this.intToBtc(period['open']), - close: this.intToBtc(period['close']), - high: this.intToBtc(period['high']), - low: this.intToBtc(period['low']), - avg: this.intToBtc(period['avg']), - volume_right: this.intToBtc(period['volume_right']), - volume_left: this.intToBtc(period['volume_left']), - }); - } - } - - return hloc; + return intervals; } private getTradesByCriteria( - market: string | null, + market: string | undefined, timestamp_to: number, timestamp_from: number, trade_id_to: string | undefined, @@ -422,7 +564,7 @@ class BisqMarketsApi { return matches; } - private intervalStart(ts: number, interval: string) { + private intervalStart(ts: number, interval: string): number { switch (interval) { case 'minute': return (ts - (ts % 60)); diff --git a/backend/src/api/bisq/markets.ts b/backend/src/api/bisq/markets.ts index d9902a00b..880d930ae 100644 --- a/backend/src/api/bisq/markets.ts +++ b/backend/src/api/bisq/markets.ts @@ -4,7 +4,7 @@ import { OffsersData, TradesData, Currency } from './interfaces'; import bisqMarket from './markets-api'; class Bisq { - private static MARKET_JSON_PATH = config.BSQ_MARKETS_DATA_PATH + '/btc_mainnet/db'; + private static MARKET_JSON_PATH = config.BISQ_MARKETS_DATA_PATH + '/btc_mainnet/db'; private static MARKET_JSON_FILE_PATHS = { cryptoCurrency: '/crypto_currency_list.json', fiatCurrency: '/fiat_currency_list.json', diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 0aaea5212..f4939f52c 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -175,7 +175,7 @@ class Routes { const p = this.parseRequestParameters(req, constraints); if (p.error) { - res.status(501).json(this.getBisqMarketErrorResponse(p.error)); + res.status(400).json(this.getBisqMarketErrorResponse(p.error)); return; } @@ -197,7 +197,7 @@ class Routes { const p = this.parseRequestParameters(req, constraints); if (p.error) { - res.status(501).json(this.getBisqMarketErrorResponse(p.error)); + res.status(400).json(this.getBisqMarketErrorResponse(p.error)); return; } @@ -256,7 +256,7 @@ class Routes { const p = this.parseRequestParameters(req, constraints); if (p.error) { - res.status(501).json(this.getBisqMarketErrorResponse(p.error)); + res.status(400).json(this.getBisqMarketErrorResponse(p.error)); return; } @@ -283,7 +283,7 @@ class Routes { const p = this.parseRequestParameters(req, constraints); if (p.error) { - res.status(501).json(this.getBisqMarketErrorResponse(p.error)); + res.status(400).json(this.getBisqMarketErrorResponse(p.error)); return; } @@ -298,7 +298,7 @@ class Routes { public getBisqMarketVolumes(req: Request, res: Response) { const constraints: RequiredSpec = { 'market': { - required: true, + required: false, types: ['@string'] }, 'interval': { @@ -313,15 +313,19 @@ class Routes { required: false, types: ['@number'] }, + 'milliseconds': { + required: false, + types: ['@boolean'] + }, }; const p = this.parseRequestParameters(req, constraints); if (p.error) { - res.status(501).json(this.getBisqMarketErrorResponse(p.error)); + res.status(400).json(this.getBisqMarketErrorResponse(p.error)); return; } - const result = bisqMarket.getVolumes(p.timestamp_from, p.timestamp_to, p.interval, p.market); + const result = bisqMarket.getVolumes(p.market, p.timestamp_from, p.timestamp_to, p.interval, p.milliseconds); if (result) { res.json(result); } else { @@ -355,7 +359,7 @@ class Routes { const p = this.parseRequestParameters(req, constraints); if (p.error) { - res.status(501).json(this.getBisqMarketErrorResponse(p.error)); + res.status(400).json(this.getBisqMarketErrorResponse(p.error)); return; } @@ -377,11 +381,11 @@ class Routes { const p = this.parseRequestParameters(req, constraints); if (p.error) { - res.status(501).json(this.getBisqMarketErrorResponse(p.error)); + res.status(400).json(this.getBisqMarketErrorResponse(p.error)); return; } - const result = bisqMarket.getCurrencies(p.market); + const result = bisqMarket.getTicker(p.market); if (result) { res.json(result); } else { diff --git a/production/mempool-config.bisq.json b/production/mempool-config.bisq.json index aeb1c9033..630790990 100644 --- a/production/mempool-config.bisq.json +++ b/production/mempool-config.bisq.json @@ -14,7 +14,7 @@ "TX_PER_SECOND_SPAN_SECONDS": 150, "ELECTRS_API_URL": "http://[::1]:3000", "BISQ_ENABLED": true, - "BSQ_BLOCKS_DATA_PATH": "/bisq/data", + "BISQ_BLOCKS_DATA_PATH": "/bisq/data", "SSL": false, "SSL_CERT_FILE_PATH": "/etc/letsencrypt/live/mysite/fullchain.pem", "SSL_KEY_FILE_PATH": "/etc/letsencrypt/live/mysite/privkey.pem"