Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bisq support #92

Merged
merged 33 commits into from Jul 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c21dad8
WIP: Bisq DAO support. Transactions list and details.
softsimon Jul 3, 2020
4ff9663
merge from master
softsimon Jul 5, 2020
8c23eae
Auto reload bisq dump file.
softsimon Jul 10, 2020
60e1b9a
Bisq explorer is now a separate module.
softsimon Jul 10, 2020
db2e293
Bisq module separation.
softsimon Jul 13, 2020
432fb9c
Address index and api.
softsimon Jul 13, 2020
87abfc3
Display network logo in the top bar.
softsimon Jul 13, 2020
b7376fb
Bisq statistics page.
softsimon Jul 14, 2020
ca0cf23
Bisq stats page.
softsimon Jul 14, 2020
3008f99
Show fiat value of totalRecieved for addresses.
softsimon Jul 14, 2020
3333b76
Rearange network dropdown.
softsimon Jul 15, 2020
e4a65bd
Corrected Liquid logo.
softsimon Jul 15, 2020
40f7eaf
Watch dir instead of json file.
softsimon Jul 15, 2020
40afa7a
Remove filename check for fs.watch
softsimon Jul 15, 2020
87e56e2
Network menu design updates.
softsimon Jul 16, 2020
c5759be
Fix responsiveness.
softsimon Jul 16, 2020
d3d3fd0
If bisq tx not found, check for regular tx and redirect to /tx/
softsimon Jul 16, 2020
ad3c295
Handle errors in block component.
softsimon Jul 16, 2020
74e9eca
Fixed bisq transaction id.
softsimon Jul 16, 2020
3ff1957
Table design and responsiveness updates.
softsimon Jul 18, 2020
d22e4a0
Adding Bisq API to About page.
softsimon Jul 18, 2020
425f415
Handle confirm transactions not present in the bisq database.
softsimon Jul 18, 2020
3df36cd
Watch for bisq dump recursively.
softsimon Jul 18, 2020
66c565a
Adding multiple fs.watch-ers to handle Bisq restarts.
softsimon Jul 18, 2020
5198c3a
Send correct content-types.
softsimon Jul 18, 2020
4e6ba18
Add proxy_pass with rewrite for /bisq/api into nginx.conf
wiz Jul 18, 2020
cfb60c0
Update Bisq API paths to /api/bisq
softsimon Jul 19, 2020
fbb8185
Responsiveness fixes.
softsimon Jul 19, 2020
c893608
Updated api docs.
softsimon Jul 19, 2020
cca6955
Refactored "features" and "fee rating" from transaction into components.
softsimon Jul 19, 2020
ee6108c
Display tx data on bisq transaction page.
softsimon Jul 19, 2020
83d821d
bisq transaction link fix
softsimon Jul 19, 2020
621ff8f
Fix for responsive bug in medium screen size mode.
softsimon Jul 19, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/mempool-config.sample.json
Expand Up @@ -13,6 +13,8 @@
"INITIAL_BLOCK_AMOUNT": 8,
"TX_PER_SECOND_SPAN_SECONDS": 150,
"ELECTRS_API_URL": "https://www.blockstream.info/testnet/api",
"BISQ_ENABLED": false,
"BSQ_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"
Expand Down
228 changes: 228 additions & 0 deletions backend/src/api/bisq.ts
@@ -0,0 +1,228 @@
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';

class Bisq {
private latestBlockHeight = 0;
private blocks: BisqBlock[] = [];
private transactions: BisqTransaction[] = [];
private transactionIndex: { [txId: string]: BisqTransaction } = {};
private blockIndex: { [hash: string]: BisqBlock } = {};
private addressIndex: { [address: string]: BisqTransaction[] } = {};
private stats: BisqStats = {
minted: 0,
burnt: 0,
addresses: 0,
unspent_txos: 0,
spent_txos: 0,
};
private price: number = 0;
private priceUpdateCallbackFunction: ((price: number) => void) | undefined;
private subdirectoryWatcher: fs.FSWatcher | undefined;

constructor() {}

startBisqService(): void {
this.loadBisqDumpFile();
setInterval(this.updatePrice.bind(this), 1000 * 60 * 60);
this.updatePrice();
this.startTopLevelDirectoryWatcher();
this.restartSubDirectoryWatcher();
}

getTransaction(txId: string): BisqTransaction | undefined {
return this.transactionIndex[txId];
}

getTransactions(start: number, length: number): [BisqTransaction[], number] {
return [this.transactions.slice(start, length + start), this.transactions.length];
}

getBlock(hash: string): BisqBlock | undefined {
return this.blockIndex[hash];
}

getAddress(hash: string): BisqTransaction[] {
return this.addressIndex[hash];
}

getBlocks(start: number, length: number): [BisqBlock[], number] {
return [this.blocks.slice(start, length + start), this.blocks.length];
}

getStats(): BisqStats {
return this.stats;
}

setPriceCallbackFunction(fn: (price: number) => void) {
this.priceUpdateCallbackFunction = fn;
}

getLatestBlockHeight(): number {
return this.latestBlockHeight;
}

private startTopLevelDirectoryWatcher() {
let fsWait: NodeJS.Timeout | null = null;
fs.watch(config.BSQ_BLOCKS_DATA_PATH, () => {
if (fsWait) {
clearTimeout(fsWait);
}
fsWait = setTimeout(() => {
console.log(`Change detected in the top level Bisq data folder. Resetting inner watcher.`);
this.restartSubDirectoryWatcher();
}, 15000);
});
}

private restartSubDirectoryWatcher() {
if (this.subdirectoryWatcher) {
this.subdirectoryWatcher.close();
}

let fsWait: NodeJS.Timeout | null = null;
this.subdirectoryWatcher = fs.watch(config.BSQ_BLOCKS_DATA_PATH + '/all', () => {
if (fsWait) {
clearTimeout(fsWait);
}
fsWait = setTimeout(() => {
console.log(`Change detected in the Bisq data folder.`);
this.loadBisqDumpFile();
}, 2000);
});
}

private updatePrice() {
request('https://markets.bisq.network/api/trades/?market=bsq_btc', { json: true }, (err, res, trades: BisqTrade[]) => {
if (err) { return console.log(err); }

const prices: number[] = [];
trades.forEach((trade) => {
prices.push(parseFloat(trade.price) * 100000000);
});
prices.sort((a, b) => a - b);
this.price = Common.median(prices);
if (this.priceUpdateCallbackFunction) {
this.priceUpdateCallbackFunction(this.price);
}
});
}

private async loadBisqDumpFile(): Promise<void> {
try {
const data = await this.loadData();
await this.loadBisqBlocksDump(data);
this.buildIndex();
this.calculateStats();
} catch (e) {
console.log('loadBisqDumpFile() error.', e.message);
}
}

private buildIndex() {
const start = new Date().getTime();
this.transactions = [];
this.transactionIndex = {};
this.addressIndex = {};

this.blocks.forEach((block) => {
/* Build block index */
if (!this.blockIndex[block.hash]) {
this.blockIndex[block.hash] = block;
}

/* Build transactions index */
block.txs.forEach((tx) => {
this.transactions.push(tx);
this.transactionIndex[tx.id] = tx;
});
});

/* Build address index */
this.transactions.forEach((tx) => {
tx.inputs.forEach((input) => {
if (!this.addressIndex[input.address]) {
this.addressIndex[input.address] = [];
}
if (this.addressIndex[input.address].indexOf(tx) === -1) {
this.addressIndex[input.address].push(tx);
}
});
tx.outputs.forEach((output) => {
if (!this.addressIndex[output.address]) {
this.addressIndex[output.address] = [];
}
if (this.addressIndex[output.address].indexOf(tx) === -1) {
this.addressIndex[output.address].push(tx);
}
});
});

const time = new Date().getTime() - start;
console.log('Bisq data index rebuilt in ' + time + ' ms');
}

private calculateStats() {
let minted = 0;
let burned = 0;
let unspent = 0;
let spent = 0;

this.transactions.forEach((tx) => {
tx.outputs.forEach((output) => {
if (output.opReturn) {
return;
}
if (output.txOutputType === 'GENESIS_OUTPUT' || output.txOutputType === 'ISSUANCE_CANDIDATE_OUTPUT' && output.isVerified) {
minted += output.bsqAmount;
}
if (output.isUnspent) {
unspent++;
} else {
spent++;
}
});
burned += tx['burntFee'];
});

this.stats = {
addresses: Object.keys(this.addressIndex).length,
minted: minted,
burnt: burned,
spent_txos: spent,
unspent_txos: unspent,
};
}

private async loadBisqBlocksDump(cacheData: string): Promise<void> {
const start = new Date().getTime();
if (cacheData && cacheData.length !== 0) {
console.log('Loading Bisq data from dump...');
const data: BisqBlocks = JSON.parse(cacheData);
if (data.blocks && data.blocks.length !== this.blocks.length) {
this.blocks = data.blocks.filter((block) => block.txs.length > 0);
this.blocks.reverse();
this.latestBlockHeight = data.chainHeight;
const time = new Date().getTime() - start;
console.log('Bisq dump loaded in ' + time + ' ms');
} else {
throw new Error(`Bisq dump didn't contain any blocks`);
}
}
}

private loadData(): Promise<string> {
return new Promise((resolve, reject) => {
fs.readFile(config.BSQ_BLOCKS_DATA_PATH + '/all/blocks.json', 'utf8', (err, data) => {
if (err) {
reject(err);
}
resolve(data);
});
});
}
}

export default new Bisq();
2 changes: 1 addition & 1 deletion backend/src/api/blocks.ts
Expand Up @@ -73,10 +73,10 @@ class Blocks {
console.log(`${found} of ${txIds.length} found in mempool. ${notFound} not found.`);

block.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0);
block.coinbaseTx = this.stripCoinbaseTransaction(transactions[0]);
transactions.sort((a, b) => b.feePerVsize - a.feePerVsize);
block.medianFee = transactions.length > 1 ? Common.median(transactions.map((tx) => tx.feePerVsize)) : 0;
block.feeRange = transactions.length > 1 ? Common.getFeesInRange(transactions, 8, 1) : [0, 0];
block.coinbaseTx = this.stripCoinbaseTransaction(transactions[0]);

this.blocks.push(block);
if (this.blocks.length > config.KEEP_BLOCK_AMOUNT) {
Expand Down
3 changes: 3 additions & 0 deletions backend/src/api/mempool.ts
Expand Up @@ -35,6 +35,9 @@ class Mempool {

public setMempool(mempoolData: { [txId: string]: TransactionExtended }) {
this.mempoolCache = mempoolData;
if (this.mempoolChangedCallback) {
this.mempoolChangedCallback(this.mempoolCache, [], []);
}
}

public async updateMemPoolInfo() {
Expand Down
6 changes: 6 additions & 0 deletions backend/src/api/websocket-handler.ts
Expand Up @@ -12,13 +12,18 @@ import { Common } from './common';
class WebsocketHandler {
private wss: WebSocket.Server | undefined;
private nativeAssetId = '6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d';
private extraInitProperties = {};

constructor() { }

setWebsocketServer(wss: WebSocket.Server) {
this.wss = wss;
}

setExtraInitProperties(property: string, value: any) {
this.extraInitProperties[property] = value;
}

setupConnectionHandling() {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
Expand Down Expand Up @@ -84,6 +89,7 @@ class WebsocketHandler {
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
'git-commit': backendInfo.gitCommitHash,
'hostname': backendInfo.hostname,
...this.extraInitProperties
}));
}

Expand Down
18 changes: 18 additions & 0 deletions backend/src/index.ts
Expand Up @@ -14,6 +14,7 @@ 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';

class Server {
wss: WebSocket.Server;
Expand Down Expand Up @@ -50,6 +51,11 @@ class Server {
fiatConversion.startService();
diskCache.loadMempoolCache();

if (config.BISQ_ENABLED) {
bisq.startBisqService();
bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price));
}

this.server.listen(config.HTTP_PORT, () => {
console.log(`Server started on port ${config.HTTP_PORT}`);
});
Expand Down Expand Up @@ -84,6 +90,18 @@ class Server {
.get(config.API_ENDPOINT + 'statistics/1y', routes.get1YStatistics.bind(routes))
.get(config.API_ENDPOINT + 'backend-info', routes.getBackendInfo)
;

if (config.BISQ_ENABLED) {
this.app
.get(config.API_ENDPOINT + 'bisq/stats', routes.getBisqStats)
.get(config.API_ENDPOINT + 'bisq/tx/:txId', routes.getBisqTransaction)
.get(config.API_ENDPOINT + 'bisq/block/:hash', routes.getBisqBlock)
.get(config.API_ENDPOINT + 'bisq/blocks/tip/height', routes.getBisqTip)
.get(config.API_ENDPOINT + 'bisq/blocks/:index/:length', routes.getBisqBlocks)
.get(config.API_ENDPOINT + 'bisq/address/:address', routes.getBisqAddress)
.get(config.API_ENDPOINT + 'bisq/txs/:index/:length', routes.getBisqTransactions)
;
}
}
}

Expand Down