From 88e5b0343017bdb0b8e6b485dab3185edee736d6 Mon Sep 17 00:00:00 2001 From: softsimon Date: Thu, 10 Sep 2020 14:46:23 +0700 Subject: [PATCH] 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();