WIP: Bisq DAO support. Transactions list and details.
This commit is contained in:
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
@@ -43,3 +43,4 @@ testem.log
|
||||
Thumbs.db
|
||||
|
||||
cache.json
|
||||
blocks.json
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"INITIAL_BLOCK_AMOUNT": 8,
|
||||
"TX_PER_SECOND_SPAN_SECONDS": 150,
|
||||
"ELECTRS_API_URL": "https://www.blockstream.info/testnet/api",
|
||||
"BISQ_ENABLED": false,
|
||||
"SSL": false,
|
||||
"SSL_CERT_FILE_PATH": "/etc/letsencrypt/live/mysite/fullchain.pem",
|
||||
"SSL_KEY_FILE_PATH": "/etc/letsencrypt/live/mysite/privkey.pem"
|
||||
|
||||
78
backend/src/api/bisq.ts
Normal file
78
backend/src/api/bisq.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import * as fs from 'fs';
|
||||
import { BisqBlocks, BisqBlock, BisqTransaction } from '../interfaces';
|
||||
|
||||
class Bisq {
|
||||
static FILE_NAME = './blocks.json';
|
||||
private latestBlockHeight = 0;
|
||||
private blocks: BisqBlock[] = [];
|
||||
private transactions: BisqTransaction[] = [];
|
||||
private transactionsIndex: { [txId: string]: BisqTransaction } = {};
|
||||
private blocksIndex: { [hash: string]: BisqBlock } = {};
|
||||
|
||||
constructor() {}
|
||||
|
||||
startBisqService(): void {
|
||||
this.loadBisqDumpFile();
|
||||
}
|
||||
|
||||
async loadBisqDumpFile(): Promise<void> {
|
||||
await this.loadBisqBlocksDump();
|
||||
this.buildIndex();
|
||||
}
|
||||
|
||||
getTransaction(txId: string): BisqTransaction | undefined {
|
||||
return this.transactionsIndex[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.blocksIndex[hash];
|
||||
}
|
||||
|
||||
private buildIndex() {
|
||||
this.blocks.forEach((block) => {
|
||||
this.blocksIndex[block.hash] = block;
|
||||
block.txs.forEach((tx) => {
|
||||
this.transactions.push(tx);
|
||||
this.transactionsIndex[tx.id] = tx;
|
||||
});
|
||||
});
|
||||
this.blocks.reverse();
|
||||
this.transactions.reverse();
|
||||
console.log('Bisq data index rebuilt');
|
||||
}
|
||||
|
||||
private async loadBisqBlocksDump() {
|
||||
const start = new Date().getTime();
|
||||
const cacheData = await this.loadData();
|
||||
if (cacheData) {
|
||||
console.log('Parsing Bisq data from dump file');
|
||||
const data: BisqBlocks = JSON.parse(cacheData);
|
||||
if (data.blocks) {
|
||||
this.blocks = data.blocks;
|
||||
this.latestBlockHeight = data.chainHeight;
|
||||
const end = new Date().getTime();
|
||||
const time = end - start;
|
||||
console.log('Loaded bisq dump 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(Bisq.FILE_NAME, 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Bisq();
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
@@ -50,6 +51,10 @@ class Server {
|
||||
fiatConversion.startService();
|
||||
diskCache.loadMempoolCache();
|
||||
|
||||
if (config.BISQ_ENABLED) {
|
||||
bisq.startBisqService();
|
||||
}
|
||||
|
||||
this.server.listen(config.HTTP_PORT, () => {
|
||||
console.log(`Server started on port ${config.HTTP_PORT}`);
|
||||
});
|
||||
@@ -84,6 +89,14 @@ 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/tx/:txId', routes.getBisqTransaction)
|
||||
.get(config.API_ENDPOINT + 'bisq/block/:hash', routes.getBisqBlock)
|
||||
.get(config.API_ENDPOINT + 'bisq/txs/:index/:length', routes.getBisqTransactions)
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -230,3 +230,77 @@ export interface VbytesPerSecond {
|
||||
unixTime: number;
|
||||
vSize: number;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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';
|
||||
|
||||
class Routes {
|
||||
private cache = {};
|
||||
@@ -85,6 +86,36 @@ class Routes {
|
||||
public getBackendInfo(req: Request, res: Response) {
|
||||
res.send(backendInfo.getBackendInfo());
|
||||
}
|
||||
|
||||
public getBisqTransaction(req: Request, res: Response) {
|
||||
const result = bisq.getTransaction(req.params.txId);
|
||||
if (result) {
|
||||
res.send(result);
|
||||
} else {
|
||||
res.status(404).send('Bisq transaction not found');
|
||||
}
|
||||
}
|
||||
|
||||
public getBisqTransactions(req: Request, res: Response) {
|
||||
const index = parseInt(req.params.index, 10) || 0;
|
||||
const length = parseInt(req.params.length, 10) > 100 ? 100 : parseInt(req.params.length, 10) || 25;
|
||||
const [transactions, count] = bisq.getTransactions(index, length);
|
||||
if (transactions) {
|
||||
res.header('X-Total-Count', count.toString());
|
||||
res.send(transactions);
|
||||
} else {
|
||||
res.status(404).send('Bisq transaction not found');
|
||||
}
|
||||
}
|
||||
|
||||
public getBisqBlock(req: Request, res: Response) {
|
||||
const result = bisq.getBlock(req['hash']);
|
||||
if (result) {
|
||||
res.send(result);
|
||||
} else {
|
||||
res.status(404).send('Bisq block not found');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new Routes();
|
||||
|
||||
Reference in New Issue
Block a user