mirror of
https://github.com/mempool/mempool.git
synced 2024-12-27 08:44:26 +01:00
commit
222b7b9dd6
@ -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"
|
||||
|
228
backend/src/api/bisq.ts
Normal file
228
backend/src/api/bisq.ts
Normal file
@ -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();
|
@ -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() {
|
||||
|
@ -12,6 +12,7 @@ import { Common } from './common';
|
||||
class WebsocketHandler {
|
||||
private wss: WebSocket.Server | undefined;
|
||||
private nativeAssetId = '6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d';
|
||||
private extraInitProperties = {};
|
||||
|
||||
constructor() { }
|
||||
|
||||
@ -19,6 +20,10 @@ class WebsocketHandler {
|
||||
this.wss = wss;
|
||||
}
|
||||
|
||||
setExtraInitProperties(property: string, value: any) {
|
||||
this.extraInitProperties[property] = value;
|
||||
}
|
||||
|
||||
setupConnectionHandling() {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
@ -84,6 +89,7 @@ class WebsocketHandler {
|
||||
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
|
||||
'git-commit': backendInfo.gitCommitHash,
|
||||
'hostname': backendInfo.hostname,
|
||||
...this.extraInitProperties
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -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,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}`);
|
||||
});
|
||||
@ -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)
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -230,3 +230,95 @@ 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -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 = {};
|
||||
@ -25,42 +26,42 @@ class Routes {
|
||||
|
||||
public async get2HStatistics(req: Request, res: Response) {
|
||||
const result = await statistics.$list2H();
|
||||
res.send(result);
|
||||
res.json(result);
|
||||
}
|
||||
|
||||
public get24HStatistics(req: Request, res: Response) {
|
||||
res.send(this.cache['24h']);
|
||||
res.json(this.cache['24h']);
|
||||
}
|
||||
|
||||
public get1WHStatistics(req: Request, res: Response) {
|
||||
res.send(this.cache['1w']);
|
||||
res.json(this.cache['1w']);
|
||||
}
|
||||
|
||||
public get1MStatistics(req: Request, res: Response) {
|
||||
res.send(this.cache['1m']);
|
||||
res.json(this.cache['1m']);
|
||||
}
|
||||
|
||||
public get3MStatistics(req: Request, res: Response) {
|
||||
res.send(this.cache['3m']);
|
||||
res.json(this.cache['3m']);
|
||||
}
|
||||
|
||||
public get6MStatistics(req: Request, res: Response) {
|
||||
res.send(this.cache['6m']);
|
||||
res.json(this.cache['6m']);
|
||||
}
|
||||
|
||||
public get1YStatistics(req: Request, res: Response) {
|
||||
res.send(this.cache['1y']);
|
||||
res.json(this.cache['1y']);
|
||||
}
|
||||
|
||||
public async getRecommendedFees(req: Request, res: Response) {
|
||||
const result = feeApi.getRecommendedFee();
|
||||
res.send(result);
|
||||
res.json(result);
|
||||
}
|
||||
|
||||
public getMempoolBlocks(req: Request, res: Response) {
|
||||
try {
|
||||
const result = mempoolBlocks.getMempoolBlocks();
|
||||
res.send(result);
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e.message);
|
||||
}
|
||||
@ -79,11 +80,65 @@ class Routes {
|
||||
}
|
||||
|
||||
const times = mempool.getFirstSeenForTransactions(txIds);
|
||||
res.send(times);
|
||||
res.json(times);
|
||||
}
|
||||
|
||||
public getBackendInfo(req: Request, res: Response) {
|
||||
res.send(backendInfo.getBackendInfo());
|
||||
res.json(backendInfo.getBackendInfo());
|
||||
}
|
||||
|
||||
public getBisqStats(req: Request, res: Response) {
|
||||
const result = bisq.getStats();
|
||||
res.json(result);
|
||||
}
|
||||
|
||||
public getBisqTip(req: Request, res: Response) {
|
||||
const result = bisq.getLatestBlockHeight();
|
||||
res.type('text/plain');
|
||||
res.send(result.toString());
|
||||
}
|
||||
|
||||
public getBisqTransaction(req: Request, res: Response) {
|
||||
const result = bisq.getTransaction(req.params.txId);
|
||||
if (result) {
|
||||
res.json(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);
|
||||
res.header('X-Total-Count', count.toString());
|
||||
res.json(transactions);
|
||||
}
|
||||
|
||||
public getBisqBlock(req: Request, res: Response) {
|
||||
const result = bisq.getBlock(req.params.hash);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(404).send('Bisq block not found');
|
||||
}
|
||||
}
|
||||
|
||||
public getBisqBlocks(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.getBlocks(index, length);
|
||||
res.header('X-Total-Count', count.toString());
|
||||
res.json(transactions);
|
||||
}
|
||||
|
||||
public getBisqAddress(req: Request, res: Response) {
|
||||
const result = bisq.getAddress(req.params.address.substr(1));
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(404).send('Bisq address not found');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"TESTNET_ENABLED": false,
|
||||
"LIQUID_ENABLED": false,
|
||||
"BISQ_ENABLED": false,
|
||||
"ELCTRS_ITEMS_PER_PAGE": 25,
|
||||
"KEEP_BLOCKS_AMOUNT": 8
|
||||
}
|
@ -40,6 +40,10 @@
|
||||
"@angular/platform-browser": "~9.1.0",
|
||||
"@angular/platform-browser-dynamic": "~9.1.0",
|
||||
"@angular/router": "~9.1.0",
|
||||
"@fortawesome/angular-fontawesome": "^0.6.1",
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.29",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.28",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.13.0",
|
||||
"@ng-bootstrap/ng-bootstrap": "^6.1.0",
|
||||
"@types/qrcode": "^1.3.4",
|
||||
"bootstrap": "4.5.0",
|
||||
|
@ -1,18 +1,25 @@
|
||||
{
|
||||
"/api": {
|
||||
"/api/v1": {
|
||||
"target": "http://localhost:8999/",
|
||||
"secure": false
|
||||
},
|
||||
"/ws": {
|
||||
"/api/v1/ws": {
|
||||
"target": "http://localhost:8999/",
|
||||
"secure": false,
|
||||
"ws": true
|
||||
},
|
||||
"/electrs": {
|
||||
"target": "https://www.blockstream.info/testnet/api/",
|
||||
"/bisq/api": {
|
||||
"target": "http://localhost:8999/",
|
||||
"secure": false,
|
||||
"pathRewrite": {
|
||||
"^/electrs": ""
|
||||
"^/bisq/api": "/api/v1/bisq"
|
||||
}
|
||||
},
|
||||
"/api": {
|
||||
"target": "http://localhost:50001/",
|
||||
"secure": false,
|
||||
"pathRewrite": {
|
||||
"^/api": ""
|
||||
}
|
||||
}
|
||||
}
|
@ -179,6 +179,11 @@ const routes: Routes = [
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'bisq',
|
||||
component: MasterPageComponent,
|
||||
loadChildren: () => import('./bisq/bisq.module').then(m => m.BisqModule)
|
||||
},
|
||||
{
|
||||
path: 'tv',
|
||||
component: TelevisionComponent,
|
||||
|
@ -37,13 +37,15 @@ export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 7
|
||||
interface Env {
|
||||
TESTNET_ENABLED: boolean;
|
||||
LIQUID_ENABLED: boolean;
|
||||
BISQ_ENABLED: boolean;
|
||||
ELCTRS_ITEMS_PER_PAGE: number;
|
||||
KEEP_BLOCKS_AMOUNT: number;
|
||||
};
|
||||
}
|
||||
|
||||
const defaultEnv: Env = {
|
||||
'TESTNET_ENABLED': false,
|
||||
'LIQUID_ENABLED': false,
|
||||
'BISQ_ENABLED': false,
|
||||
'ELCTRS_ITEMS_PER_PAGE': 25,
|
||||
'KEEP_BLOCKS_AMOUNT': 8
|
||||
};
|
||||
|
@ -3,7 +3,7 @@ import { NgModule } from '@angular/core';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { NgbButtonsModule, NgbTooltipModule, NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgbButtonsModule, NgbPaginationModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
@ -11,25 +11,17 @@ import { AppComponent } from './components/app/app.component';
|
||||
|
||||
import { StartComponent } from './components/start/start.component';
|
||||
import { ElectrsApiService } from './services/electrs-api.service';
|
||||
import { BytesPipe } from './pipes/bytes-pipe/bytes.pipe';
|
||||
import { VbytesPipe } from './pipes/bytes-pipe/vbytes.pipe';
|
||||
import { WuBytesPipe } from './pipes/bytes-pipe/wubytes.pipe';
|
||||
import { TransactionComponent } from './components/transaction/transaction.component';
|
||||
import { TransactionsListComponent } from './components/transactions-list/transactions-list.component';
|
||||
import { AmountComponent } from './components/amount/amount.component';
|
||||
import { StateService } from './services/state.service';
|
||||
import { BlockComponent } from './components/block/block.component';
|
||||
import { ShortenStringPipe } from './pipes/shorten-string-pipe/shorten-string.pipe';
|
||||
import { AddressComponent } from './components/address/address.component';
|
||||
import { SearchFormComponent } from './components/search-form/search-form.component';
|
||||
import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component';
|
||||
import { WebsocketService } from './services/websocket.service';
|
||||
import { TimeSinceComponent } from './components/time-since/time-since.component';
|
||||
import { AddressLabelsComponent } from './components/address-labels/address-labels.component';
|
||||
import { MempoolBlocksComponent } from './components/mempool-blocks/mempool-blocks.component';
|
||||
import { CeilPipe } from './pipes/math-ceil/math-ceil.pipe';
|
||||
import { QrcodeComponent } from './components/qrcode/qrcode.component';
|
||||
import { ClipboardComponent } from './components/clipboard/clipboard.component';
|
||||
import { MasterPageComponent } from './components/master-page/master-page.component';
|
||||
import { AboutComponent } from './components/about/about.component';
|
||||
import { TelevisionComponent } from './components/television/television.component';
|
||||
@ -39,19 +31,16 @@ import { BlockchainBlocksComponent } from './components/blockchain-blocks/blockc
|
||||
import { BlockchainComponent } from './components/blockchain/blockchain.component';
|
||||
import { FooterComponent } from './components/footer/footer.component';
|
||||
import { AudioService } from './services/audio.service';
|
||||
import { FiatComponent } from './fiat/fiat.component';
|
||||
import { MempoolBlockComponent } from './components/mempool-block/mempool-block.component';
|
||||
import { FeeDistributionGraphComponent } from './components/fee-distribution-graph/fee-distribution-graph.component';
|
||||
import { TimespanComponent } from './components/timespan/timespan.component';
|
||||
import { SeoService } from './services/seo.service';
|
||||
import { MempoolGraphComponent } from './components/mempool-graph/mempool-graph.component';
|
||||
import { AssetComponent } from './components/asset/asset.component';
|
||||
import { ScriptpubkeyTypePipe } from './pipes/scriptpubkey-type-pipe/scriptpubkey-type.pipe';
|
||||
import { AssetsComponent } from './assets/assets.component';
|
||||
import { RelativeUrlPipe } from './pipes/relative-url/relative-url.pipe';
|
||||
import { MinerComponent } from './pipes/miner/miner.component';
|
||||
import { Hex2asciiPipe } from './pipes/hex2ascii/hex2ascii.pipe';
|
||||
import { StatusViewComponent } from './components/status-view/status-view.component';
|
||||
import { MinerComponent } from './components/miner/miner.component';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -66,33 +55,21 @@ import { StatusViewComponent } from './components/status-view/status-view.compon
|
||||
TransactionComponent,
|
||||
BlockComponent,
|
||||
TransactionsListComponent,
|
||||
BytesPipe,
|
||||
VbytesPipe,
|
||||
WuBytesPipe,
|
||||
CeilPipe,
|
||||
ShortenStringPipe,
|
||||
AddressComponent,
|
||||
AmountComponent,
|
||||
SearchFormComponent,
|
||||
LatestBlocksComponent,
|
||||
TimeSinceComponent,
|
||||
TimespanComponent,
|
||||
AddressLabelsComponent,
|
||||
MempoolBlocksComponent,
|
||||
QrcodeComponent,
|
||||
ClipboardComponent,
|
||||
ChartistComponent,
|
||||
FooterComponent,
|
||||
FiatComponent,
|
||||
MempoolBlockComponent,
|
||||
FeeDistributionGraphComponent,
|
||||
MempoolGraphComponent,
|
||||
AssetComponent,
|
||||
ScriptpubkeyTypePipe,
|
||||
AssetsComponent,
|
||||
RelativeUrlPipe,
|
||||
MinerComponent,
|
||||
Hex2asciiPipe,
|
||||
StatusViewComponent,
|
||||
],
|
||||
imports: [
|
||||
@ -102,15 +79,15 @@ import { StatusViewComponent } from './components/status-view/status-view.compon
|
||||
ReactiveFormsModule,
|
||||
BrowserAnimationsModule,
|
||||
NgbButtonsModule,
|
||||
NgbTooltipModule,
|
||||
NgbPaginationModule,
|
||||
NgbDropdownModule,
|
||||
InfiniteScrollModule,
|
||||
SharedModule,
|
||||
],
|
||||
providers: [
|
||||
ElectrsApiService,
|
||||
StateService,
|
||||
WebsocketService,
|
||||
VbytesPipe,
|
||||
AudioService,
|
||||
SeoService,
|
||||
],
|
||||
|
@ -67,7 +67,7 @@ export class AssetsComponent implements OnInit {
|
||||
});
|
||||
this.assets = this.assets.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
||||
this.assetsCache = this.assets;
|
||||
this.searchForm.controls['searchText'].enable();
|
||||
this.searchForm.get('searchText').enable();
|
||||
this.filteredAssets = this.assets.slice(0, this.itemsPerPage);
|
||||
this.isLoading = false;
|
||||
},
|
||||
|
106
frontend/src/app/bisq/bisq-address/bisq-address.component.html
Normal file
106
frontend/src/app/bisq/bisq-address/bisq-address.component.html
Normal file
@ -0,0 +1,106 @@
|
||||
<div class="container-xl">
|
||||
<h1 style="float: left;">Address</h1>
|
||||
<a [routerLink]="['/address/' | relativeUrl, addressString]" style="line-height: 56px; margin-left: 10px;">
|
||||
<span class="d-inline d-lg-none">{{ addressString | shortenString : 24 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ addressString }}</span>
|
||||
</a>
|
||||
<app-clipboard [text]="addressString"></app-clipboard>
|
||||
<br>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<ng-template [ngIf]="!isLoadingAddress && !error">
|
||||
<div class="box">
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Total received</td>
|
||||
<td>{{ totalReceived / 100 | number: '1.2-2' }} BSQ</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total sent</td>
|
||||
<td>{{ totalSent / 100 | number: '1.2-2' }} BSQ</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Final balance</td>
|
||||
<td>{{ (totalReceived - totalSent) / 100 | number: '1.2-2' }} BSQ (<app-bsq-amount [bsq]="totalReceived - totalSent" [forceFiat]="true" [green]="true"></app-bsq-amount>)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="w-100 d-block d-md-none"></div>
|
||||
<div class="col qrcode-col">
|
||||
<div class="qr-wrapper">
|
||||
<app-qrcode [data]="addressString"></app-qrcode>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<h2>{{ transactions.length | number }} transactions</h2>
|
||||
|
||||
<ng-template ngFor let-tx [ngForOf]="transactions">
|
||||
|
||||
<div class="header-bg box" style="padding: 10px; margin-bottom: 10px;">
|
||||
<a [routerLink]="['/tx/' | relativeUrl, tx.id]" [state]="{ data: tx }">
|
||||
<span style="float: left;" class="d-block d-md-none">{{ tx.id | shortenString : 16 }}</span>
|
||||
<span style="float: left;" class="d-none d-md-block">{{ tx.id }}</span>
|
||||
</a>
|
||||
<div class="float-right">
|
||||
{{ tx.time | date:'yyyy-MM-dd HH:mm' }}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<app-bisq-transfers [tx]="tx" [showConfirmations]="true"></app-bisq-transfers>
|
||||
|
||||
<br>
|
||||
</ng-template>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="isLoadingAddress && !error">
|
||||
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="w-100 d-block d-md-none"></div>
|
||||
<div class="col">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="error">
|
||||
<div class="text-center">
|
||||
Error loading address data.
|
||||
<br>
|
||||
<i>{{ error.error }}</i>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
@ -0,0 +1,23 @@
|
||||
.qr-wrapper {
|
||||
background-color: #FFF;
|
||||
padding: 10px;
|
||||
padding-bottom: 5px;
|
||||
display: inline-block;
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.qrcode-col {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
@media (max-width: 575.98px) {
|
||||
.qrcode-col {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qrcode-col > div {
|
||||
margin-top: 20px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
82
frontend/src/app/bisq/bisq-address/bisq-address.component.ts
Normal file
82
frontend/src/app/bisq/bisq-address/bisq-address.component.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { switchMap, filter, catchError } from 'rxjs/operators';
|
||||
import { ParamMap, ActivatedRoute } from '@angular/router';
|
||||
import { Subscription, of } from 'rxjs';
|
||||
import { BisqTransaction } from '../bisq.interfaces';
|
||||
import { BisqApiService } from '../bisq-api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-address',
|
||||
templateUrl: './bisq-address.component.html',
|
||||
styleUrls: ['./bisq-address.component.scss']
|
||||
})
|
||||
export class BisqAddressComponent implements OnInit, OnDestroy {
|
||||
transactions: BisqTransaction[];
|
||||
addressString: string;
|
||||
isLoadingAddress = true;
|
||||
error: any;
|
||||
mainSubscription: Subscription;
|
||||
|
||||
totalReceived = 0;
|
||||
totalSent = 0;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private seoService: SeoService,
|
||||
private bisqApiService: BisqApiService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.mainSubscription = this.route.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.error = undefined;
|
||||
this.isLoadingAddress = true;
|
||||
this.transactions = null;
|
||||
document.body.scrollTo(0, 0);
|
||||
this.addressString = params.get('id') || '';
|
||||
this.seoService.setTitle('Address: ' + this.addressString, true);
|
||||
|
||||
return this.bisqApiService.getAddress$(this.addressString)
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
this.isLoadingAddress = false;
|
||||
this.error = err;
|
||||
console.log(err);
|
||||
return of(null);
|
||||
})
|
||||
);
|
||||
}),
|
||||
filter((transactions) => transactions !== null)
|
||||
)
|
||||
.subscribe((transactions: BisqTransaction[]) => {
|
||||
this.transactions = transactions;
|
||||
this.updateChainStats();
|
||||
this.isLoadingAddress = false;
|
||||
},
|
||||
(error) => {
|
||||
console.log(error);
|
||||
this.error = error;
|
||||
this.isLoadingAddress = false;
|
||||
});
|
||||
}
|
||||
|
||||
updateChainStats() {
|
||||
const shortenedAddress = this.addressString.substr(1);
|
||||
|
||||
this.totalSent = this.transactions.reduce((acc, tx) =>
|
||||
acc + tx.inputs
|
||||
.filter((input) => input.address === shortenedAddress)
|
||||
.reduce((a, input) => a + input.bsqAmount, 0), 0);
|
||||
|
||||
this.totalReceived = this.transactions.reduce((acc, tx) =>
|
||||
acc + tx.outputs
|
||||
.filter((output) => output.address === shortenedAddress)
|
||||
.reduce((a, output) => a + output.bsqAmount, 0), 0);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.mainSubscription.unsubscribe();
|
||||
}
|
||||
}
|
41
frontend/src/app/bisq/bisq-api.service.ts
Normal file
41
frontend/src/app/bisq/bisq-api.service.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpResponse } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BisqTransaction, BisqBlock, BisqStats } from './bisq.interfaces';
|
||||
|
||||
const API_BASE_URL = '/bisq/api';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class BisqApiService {
|
||||
apiBaseUrl: string;
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
) { }
|
||||
|
||||
getStats$(): Observable<BisqStats> {
|
||||
return this.httpClient.get<BisqStats>(API_BASE_URL + '/stats');
|
||||
}
|
||||
|
||||
getTransaction$(txId: string): Observable<BisqTransaction> {
|
||||
return this.httpClient.get<BisqTransaction>(API_BASE_URL + '/tx/' + txId);
|
||||
}
|
||||
|
||||
listTransactions$(start: number, length: number): Observable<HttpResponse<BisqTransaction[]>> {
|
||||
return this.httpClient.get<BisqTransaction[]>(API_BASE_URL + `/txs/${start}/${length}`, { observe: 'response' });
|
||||
}
|
||||
|
||||
getBlock$(hash: string): Observable<BisqBlock> {
|
||||
return this.httpClient.get<BisqBlock>(API_BASE_URL + '/block/' + hash);
|
||||
}
|
||||
|
||||
listBlocks$(start: number, length: number): Observable<HttpResponse<BisqBlock[]>> {
|
||||
return this.httpClient.get<BisqBlock[]>(API_BASE_URL + `/blocks/${start}/${length}`, { observe: 'response' });
|
||||
}
|
||||
|
||||
getAddress$(address: string): Observable<BisqTransaction[]> {
|
||||
return this.httpClient.get<BisqTransaction[]>(API_BASE_URL + '/address/' + address);
|
||||
}
|
||||
}
|
108
frontend/src/app/bisq/bisq-block/bisq-block.component.html
Normal file
108
frontend/src/app/bisq/bisq-block/bisq-block.component.html
Normal file
@ -0,0 +1,108 @@
|
||||
<div class="container-xl">
|
||||
|
||||
<div class="title-block">
|
||||
<h1>Block <ng-template [ngIf]="blockHeight"><a [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockHeight }}</a></ng-template></h1>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<ng-template [ngIf]="!isLoading && !error">
|
||||
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width">Hash</td>
|
||||
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" title="{{ block.hash }}">{{ block.hash | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.hash"></app-clipboard></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Timestamp</td>
|
||||
<td>
|
||||
{{ block.time | date:'yyyy-MM-dd HH:mm' }}
|
||||
<div class="lg-inline">
|
||||
<i>(<app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since> ago)</i>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width">Previous hash</td>
|
||||
<td><a [routerLink]="['/block/' | relativeUrl, block.previousBlockHash]" title="{{ block.hash }}">{{ block.previousBlockHash | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.previousBlockHash"></app-clipboard></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<br>
|
||||
|
||||
<h2>{{ block.txs.length | number }} transactions</h2>
|
||||
|
||||
<ng-template ngFor let-tx [ngForOf]="block.txs">
|
||||
|
||||
<div class="header-bg box" style="padding: 10px; margin-bottom: 10px;">
|
||||
<a [routerLink]="['/tx/' | relativeUrl, tx.id]" [state]="{ data: tx }">
|
||||
<span style="float: left;" class="d-block d-md-none">{{ tx.id | shortenString : 16 }}</span>
|
||||
<span style="float: left;" class="d-none d-md-block">{{ tx.id }}</span>
|
||||
</a>
|
||||
<div class="float-right">
|
||||
{{ tx.time | date:'yyyy-MM-dd HH:mm' }}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<app-bisq-transfers [tx]="tx" [showConfirmations]="true"></app-bisq-transfers>
|
||||
|
||||
<br>
|
||||
</ng-template>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="isLoading && !error">
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width">Hash</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Timestamp</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width">Previous hash</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="error">
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="text-center">
|
||||
Error loading block
|
||||
<br>
|
||||
<i>{{ error.status }}: {{ error.statusText }}</i>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
10
frontend/src/app/bisq/bisq-block/bisq-block.component.scss
Normal file
10
frontend/src/app/bisq/bisq-block/bisq-block.component.scss
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
.td-width {
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.td-width {
|
||||
width: 140px;
|
||||
}
|
||||
}
|
98
frontend/src/app/bisq/bisq-block/bisq-block.component.ts
Normal file
98
frontend/src/app/bisq/bisq-block/bisq-block.component.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { BisqBlock } from 'src/app/bisq/bisq.interfaces';
|
||||
import { Location } from '@angular/common';
|
||||
import { BisqApiService } from '../bisq-api.service';
|
||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import { Subscription, of } from 'rxjs';
|
||||
import { switchMap, catchError } from 'rxjs/operators';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { ElectrsApiService } from 'src/app/services/electrs-api.service';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-block',
|
||||
templateUrl: './bisq-block.component.html',
|
||||
styleUrls: ['./bisq-block.component.scss']
|
||||
})
|
||||
export class BisqBlockComponent implements OnInit, OnDestroy {
|
||||
block: BisqBlock;
|
||||
subscription: Subscription;
|
||||
blockHash = '';
|
||||
blockHeight = 0;
|
||||
isLoading = true;
|
||||
error: HttpErrorResponse | null;
|
||||
|
||||
constructor(
|
||||
private bisqApiService: BisqApiService,
|
||||
private route: ActivatedRoute,
|
||||
private seoService: SeoService,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private router: Router,
|
||||
private location: Location,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.subscription = this.route.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
const blockHash = params.get('id') || '';
|
||||
document.body.scrollTo(0, 0);
|
||||
this.isLoading = true;
|
||||
this.error = null;
|
||||
if (history.state.data && history.state.data.blockHeight) {
|
||||
this.blockHeight = history.state.data.blockHeight;
|
||||
}
|
||||
if (history.state.data && history.state.data.block) {
|
||||
this.blockHeight = history.state.data.block.height;
|
||||
return of(history.state.data.block);
|
||||
}
|
||||
|
||||
let isBlockHeight = false;
|
||||
if (/^[0-9]+$/.test(blockHash)) {
|
||||
isBlockHeight = true;
|
||||
} else {
|
||||
this.blockHash = blockHash;
|
||||
}
|
||||
|
||||
if (isBlockHeight) {
|
||||
return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockHash, 10))
|
||||
.pipe(
|
||||
switchMap((hash) => {
|
||||
if (!hash) {
|
||||
return;
|
||||
}
|
||||
this.blockHash = hash;
|
||||
this.location.replaceState(
|
||||
this.router.createUrlTree(['/bisq/block/', hash]).toString()
|
||||
);
|
||||
return this.bisqApiService.getBlock$(this.blockHash)
|
||||
.pipe(catchError(this.caughtHttpError.bind(this)));
|
||||
}),
|
||||
catchError(this.caughtHttpError.bind(this))
|
||||
);
|
||||
}
|
||||
|
||||
return this.bisqApiService.getBlock$(this.blockHash)
|
||||
.pipe(catchError(this.caughtHttpError.bind(this)));
|
||||
})
|
||||
)
|
||||
.subscribe((block: BisqBlock) => {
|
||||
if (!block) {
|
||||
return;
|
||||
}
|
||||
this.isLoading = false;
|
||||
this.blockHeight = block.height;
|
||||
this.seoService.setTitle('Block: #' + block.height + ': ' + block.hash, true);
|
||||
this.block = block;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
caughtHttpError(err: HttpErrorResponse){
|
||||
this.error = err;
|
||||
return of(null);
|
||||
}
|
||||
}
|
36
frontend/src/app/bisq/bisq-blocks/bisq-blocks.component.html
Normal file
36
frontend/src/app/bisq/bisq-blocks/bisq-blocks.component.html
Normal file
@ -0,0 +1,36 @@
|
||||
<div class="container-xl">
|
||||
<h1 style="float: left;">Blocks</h1>
|
||||
<br>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<th style="width: 25%;">Height</th>
|
||||
<th style="width: 25%;">Confirmed</th>
|
||||
<th style="width: 25%;">Total Sent</th>
|
||||
<th class="d-none d-md-block" style="width: 25%;">Transactions</th>
|
||||
</thead>
|
||||
<tbody *ngIf="!isLoading; else loadingTmpl">
|
||||
<tr *ngFor="let block of blocks; trackBy: trackByFn">
|
||||
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
|
||||
<td><app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since> ago</td>
|
||||
<td>{{ calculateTotalOutput(block) / 100 | number: '1.2-2' }}<span class="d-none d-md-inline"> BSQ</span></td>
|
||||
<td class="d-none d-md-block">{{ block.txs.length }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<ngb-pagination [size]="paginationSize" [collectionSize]="totalCount" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true"></ngb-pagination>
|
||||
|
||||
</div>
|
||||
|
||||
<ng-template #loadingTmpl>
|
||||
<tr *ngFor="let i of loadingItems">
|
||||
<td *ngFor="let j of [1, 2, 3, 4, 5]"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</ng-template>
|
71
frontend/src/app/bisq/bisq-blocks/bisq-blocks.component.ts
Normal file
71
frontend/src/app/bisq/bisq-blocks/bisq-blocks.component.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { BisqApiService } from '../bisq-api.service';
|
||||
import { switchMap, tap } from 'rxjs/operators';
|
||||
import { Subject } from 'rxjs';
|
||||
import { BisqBlock, BisqOutput, BisqTransaction } from '../bisq.interfaces';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-blocks',
|
||||
templateUrl: './bisq-blocks.component.html',
|
||||
styleUrls: ['./bisq-blocks.component.scss']
|
||||
})
|
||||
export class BisqBlocksComponent implements OnInit {
|
||||
blocks: BisqBlock[];
|
||||
totalCount: number;
|
||||
page = 1;
|
||||
itemsPerPage: number;
|
||||
contentSpace = window.innerHeight - (165 + 75);
|
||||
fiveItemsPxSize = 250;
|
||||
loadingItems: number[];
|
||||
isLoading = true;
|
||||
// @ts-ignore
|
||||
paginationSize: 'sm' | 'lg' = 'md';
|
||||
paginationMaxSize = 10;
|
||||
|
||||
pageSubject$ = new Subject<number>();
|
||||
|
||||
constructor(
|
||||
private bisqApiService: BisqApiService,
|
||||
private seoService: SeoService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle('Blocks', true);
|
||||
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
|
||||
this.loadingItems = Array(this.itemsPerPage);
|
||||
if (document.body.clientWidth < 768) {
|
||||
this.paginationSize = 'sm';
|
||||
this.paginationMaxSize = 3;
|
||||
}
|
||||
|
||||
this.pageSubject$
|
||||
.pipe(
|
||||
tap(() => this.isLoading = true),
|
||||
switchMap((page) => this.bisqApiService.listBlocks$((page - 1) * this.itemsPerPage, this.itemsPerPage))
|
||||
)
|
||||
.subscribe((response) => {
|
||||
this.isLoading = false;
|
||||
this.blocks = response.body;
|
||||
this.totalCount = parseInt(response.headers.get('x-total-count'), 10);
|
||||
}, (error) => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
this.pageSubject$.next(1);
|
||||
}
|
||||
|
||||
calculateTotalOutput(block: BisqBlock): number {
|
||||
return block.txs.reduce((a: number, tx: BisqTransaction) =>
|
||||
a + tx.outputs.reduce((acc: number, output: BisqOutput) => acc + output.bsqAmount, 0), 0
|
||||
);
|
||||
}
|
||||
|
||||
trackByFn(index: number) {
|
||||
return index;
|
||||
}
|
||||
|
||||
pageChange(page: number) {
|
||||
this.pageSubject$.next(page);
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
<router-outlet></router-outlet>
|
@ -0,0 +1,18 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-explorer',
|
||||
templateUrl: './bisq-explorer.component.html',
|
||||
styleUrls: ['./bisq-explorer.component.scss']
|
||||
})
|
||||
export class BisqExplorerComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks']);
|
||||
}
|
||||
}
|
1
frontend/src/app/bisq/bisq-icon/bisq-icon.component.html
Normal file
1
frontend/src/app/bisq/bisq-icon/bisq-icon.component.html
Normal file
@ -0,0 +1 @@
|
||||
<fa-icon [icon]="iconProp" [fixedWidth]="true" [ngStyle]="{ 'color': '#' + color }"></fa-icon>
|
81
frontend/src/app/bisq/bisq-icon/bisq-icon.component.ts
Normal file
81
frontend/src/app/bisq/bisq-icon/bisq-icon.component.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
|
||||
import { IconPrefix, IconName } from '@fortawesome/fontawesome-common-types';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-icon',
|
||||
templateUrl: './bisq-icon.component.html',
|
||||
styleUrls: ['./bisq-icon.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BisqIconComponent implements OnChanges {
|
||||
@Input() txType: string;
|
||||
|
||||
iconProp: [IconPrefix, IconName] = ['fas', 'leaf'];
|
||||
color: string;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnChanges() {
|
||||
switch (this.txType) {
|
||||
case 'UNVERIFIED':
|
||||
this.iconProp[1] = 'question';
|
||||
this.color = 'ffac00';
|
||||
break;
|
||||
case 'INVALID':
|
||||
this.iconProp[1] = 'exclamation-triangle';
|
||||
this.color = 'ff4500';
|
||||
break;
|
||||
case 'GENESIS':
|
||||
this.iconProp[1] = 'rocket';
|
||||
this.color = '25B135';
|
||||
break;
|
||||
case 'TRANSFER_BSQ':
|
||||
this.iconProp[1] = 'retweet';
|
||||
this.color = 'a3a3a3';
|
||||
break;
|
||||
case 'PAY_TRADE_FEE':
|
||||
this.iconProp[1] = 'leaf';
|
||||
this.color = '689f43';
|
||||
break;
|
||||
case 'PROPOSAL':
|
||||
this.iconProp[1] = 'file-alt';
|
||||
this.color = '6c8b3b';
|
||||
break;
|
||||
case 'COMPENSATION_REQUEST':
|
||||
this.iconProp[1] = 'money-bill';
|
||||
this.color = '689f43';
|
||||
break;
|
||||
case 'REIMBURSEMENT_REQUEST':
|
||||
this.iconProp[1] = 'money-bill';
|
||||
this.color = '04a908';
|
||||
break;
|
||||
case 'BLIND_VOTE':
|
||||
this.iconProp[1] = 'eye-slash';
|
||||
this.color = '07579a';
|
||||
break;
|
||||
case 'VOTE_REVEAL':
|
||||
this.iconProp[1] = 'eye';
|
||||
this.color = '4AC5FF';
|
||||
break;
|
||||
case 'LOCKUP':
|
||||
this.iconProp[1] = 'lock';
|
||||
this.color = '0056c4';
|
||||
break;
|
||||
case 'UNLOCK':
|
||||
this.iconProp[1] = 'lock-open';
|
||||
this.color = '1d965f';
|
||||
break;
|
||||
case 'ASSET_LISTING_FEE':
|
||||
this.iconProp[1] = 'file-alt';
|
||||
this.color = '6c8b3b';
|
||||
break;
|
||||
case 'PROOF_OF_BURN':
|
||||
this.iconProp[1] = 'file-alt';
|
||||
this.color = '6c8b3b';
|
||||
break;
|
||||
default:
|
||||
this.iconProp[1] = 'question';
|
||||
this.color = 'ffac00';
|
||||
}
|
||||
}
|
||||
}
|
90
frontend/src/app/bisq/bisq-stats/bisq-stats.component.html
Normal file
90
frontend/src/app/bisq/bisq-stats/bisq-stats.component.html
Normal file
@ -0,0 +1,90 @@
|
||||
<div class="container-xl">
|
||||
<h1 style="float: left;">BSQ Statistics</h1>
|
||||
<br>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<th>Property</th>
|
||||
<th>Value</th>
|
||||
</thead>
|
||||
<tbody *ngIf="!isLoading; else loadingTemplate">
|
||||
<tr>
|
||||
<td class="td-width">Existing amount</td>
|
||||
<td>{{ (stats.minted - stats.burnt) / 100 | number: '1.2-2' }} BSQ</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Minted amount</td>
|
||||
<td>{{ stats.minted | number: '1.2-2' }} BSQ</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Burnt amount</td>
|
||||
<td>{{ stats.burnt | number: '1.2-2' }} BSQ</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Addresses</td>
|
||||
<td>{{ stats.addresses | number }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Unspent TXOs</td>
|
||||
<td>{{ stats.unspent_txos | number }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Spent TXOs</td>
|
||||
<td>{{ stats.spent_txos | number }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Price</td>
|
||||
<td><app-fiat [value]="price"></app-fiat></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Market cap</td>
|
||||
<td><app-fiat [value]="price * (stats.minted - stats.burnt) / 100"></app-fiat></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<div class="col-sm"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingTemplate>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width">Existing amount</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Minted amount</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Burnt amount</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Addresses</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Unspent TXOs</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Spent TXOs</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Price</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Market cap</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</ng-template>
|
@ -0,0 +1,9 @@
|
||||
.td-width {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.td-width {
|
||||
width: 175px;
|
||||
}
|
||||
}
|
38
frontend/src/app/bisq/bisq-stats/bisq-stats.component.ts
Normal file
38
frontend/src/app/bisq/bisq-stats/bisq-stats.component.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { BisqApiService } from '../bisq-api.service';
|
||||
import { BisqStats } from '../bisq.interfaces';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-stats',
|
||||
templateUrl: './bisq-stats.component.html',
|
||||
styleUrls: ['./bisq-stats.component.scss']
|
||||
})
|
||||
export class BisqStatsComponent implements OnInit {
|
||||
isLoading = true;
|
||||
stats: BisqStats;
|
||||
price: number;
|
||||
|
||||
constructor(
|
||||
private bisqApiService: BisqApiService,
|
||||
private seoService: SeoService,
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.seoService.setTitle('BSQ Statistics', false);
|
||||
|
||||
this.stateService.bsqPrice$
|
||||
.subscribe((bsqPrice) => {
|
||||
this.price = bsqPrice;
|
||||
});
|
||||
|
||||
this.bisqApiService.getStats$()
|
||||
.subscribe((stats) => {
|
||||
this.isLoading = false;
|
||||
this.stats = stats;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width">Inputs</td>
|
||||
<td>{{ totalInput / 100 | number: '1.2-2' }} BSQ</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Outputs</td>
|
||||
<td>{{ totalOutput / 100 | number: '1.2-2' }} BSQ</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Issuance</td>
|
||||
<td>{{ totalIssued / 100 | number: '1.2-2' }} BSQ</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody class="mobile-even">
|
||||
<tr>
|
||||
<td class="td-width">Type</td>
|
||||
<td><app-bisq-icon class="mr-1" [txType]="tx.txType"></app-bisq-icon> {{ tx.txTypeDisplayString }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Version</td>
|
||||
<td>{{ tx.txVersion }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,11 @@
|
||||
@media (max-width: 767.98px) {
|
||||
.td-width {
|
||||
width: 150px;
|
||||
}
|
||||
.mobile-even tr:nth-of-type(even) {
|
||||
background-color: #181b2d;
|
||||
}
|
||||
.mobile-even tr:nth-of-type(odd) {
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
|
||||
import { BisqTransaction } from 'src/app/bisq/bisq.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-transaction-details',
|
||||
templateUrl: './bisq-transaction-details.component.html',
|
||||
styleUrls: ['./bisq-transaction-details.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BisqTransactionDetailsComponent implements OnChanges {
|
||||
@Input() tx: BisqTransaction;
|
||||
|
||||
totalInput: number;
|
||||
totalOutput: number;
|
||||
totalIssued: number;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnChanges() {
|
||||
this.totalInput = this.tx.inputs.filter((input) => input.isVerified).reduce((acc, input) => acc + input.bsqAmount, 0);
|
||||
this.totalOutput = this.tx.outputs.filter((output) => output.isVerified).reduce((acc, output) => acc + output.bsqAmount, 0);
|
||||
this.totalIssued = this.tx.outputs
|
||||
.filter((output) => output.isVerified && output.txOutputType === 'ISSUANCE_CANDIDATE_OUTPUT')
|
||||
.reduce((acc, output) => acc + output.bsqAmount, 0);
|
||||
}
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
<div class="container-xl">
|
||||
|
||||
<h1 class="float-left mr-3 mb-md-3">Transaction</h1>
|
||||
|
||||
<ng-template [ngIf]="!isLoading && !error">
|
||||
|
||||
<button *ngIf="(latestBlock$ | async) as latestBlock" type="button" class="btn btn-sm btn-success float-right mr-2 mt-1 mt-md-3">{{ latestBlock.height - bisqTx.blockHeight + 1 }} confirmation<ng-container *ngIf="latestBlock.height - bisqTx.blockHeight + 1 > 1">s</ng-container></button>
|
||||
|
||||
<div>
|
||||
<a [routerLink]="['/bisq-tx' | relativeUrl, bisqTx.id]" style="line-height: 56px;">
|
||||
<span class="d-inline d-lg-none">{{ bisqTx.id | shortenString : 24 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ bisqTx.id }}</span>
|
||||
</a>
|
||||
<app-clipboard [text]="bisqTx.id"></app-clipboard>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width">Included in block</td>
|
||||
<td>
|
||||
<a [routerLink]="['/block/' | relativeUrl, bisqTx.blockHash]" [state]="{ data: { blockHeight: bisqTx.blockHeight } }">{{ bisqTx.blockHeight }}</a>
|
||||
<i> (<app-time-since [time]="bisqTx.time / 1000" [fastRender]="true"></app-time-since> ago)</i>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="td-width">Features</td>
|
||||
<td>
|
||||
<app-tx-features *ngIf="tx; else loadingTx" [tx]="tx"></app-tx-features>
|
||||
<ng-template #loadingTx>
|
||||
<span class="skeleton-loader"></span>
|
||||
</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width">Burnt</td>
|
||||
<td>
|
||||
{{ bisqTx.burntFee / 100 | number: '1.2-2' }} BSQ (<app-bsq-amount [bsq]="bisqTx.burntFee" [forceFiat]="true" [green]="true"></app-bsq-amount>)
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Fee per vByte</td>
|
||||
<td *ngIf="!isLoadingTx; else loadingTxFee">
|
||||
{{ tx.fee / (tx.weight / 4) | number : '1.1-1' }} sat/vB
|
||||
|
||||
<app-tx-fee-rating [tx]="tx"></app-tx-fee-rating>
|
||||
</td>
|
||||
<ng-template #loadingTxFee>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</ng-template>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<h2>Details</h2>
|
||||
|
||||
|
||||
<app-bisq-transaction-details [tx]="bisqTx"></app-bisq-transaction-details>
|
||||
|
||||
<br>
|
||||
|
||||
<h2>Inputs & Outputs</h2>
|
||||
|
||||
<app-bisq-transfers [tx]="bisqTx"></app-bisq-transfers>
|
||||
|
||||
<br>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf="isLoading && !error">
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width"><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width"><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<h2>Details</h2>
|
||||
<div class="box">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<h2>Inputs & Outputs</h2>
|
||||
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="error">
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="text-center">
|
||||
Error loading transaction
|
||||
<br><br>
|
||||
<i>{{ error.status }}: {{ error.statusText }}</i>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
@ -0,0 +1,9 @@
|
||||
.td-width {
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.td-width {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import { BisqTransaction } from 'src/app/bisq/bisq.interfaces';
|
||||
import { switchMap, map, catchError } from 'rxjs/operators';
|
||||
import { of, Observable, Subscription } from 'rxjs';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { Block, Transaction } from 'src/app/interfaces/electrs.interface';
|
||||
import { BisqApiService } from '../bisq-api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { ElectrsApiService } from 'src/app/services/electrs-api.service';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-transaction',
|
||||
templateUrl: './bisq-transaction.component.html',
|
||||
styleUrls: ['./bisq-transaction.component.scss']
|
||||
})
|
||||
export class BisqTransactionComponent implements OnInit, OnDestroy {
|
||||
bisqTx: BisqTransaction;
|
||||
tx: Transaction;
|
||||
latestBlock$: Observable<Block>;
|
||||
txId: string;
|
||||
price: number;
|
||||
isLoading = true;
|
||||
isLoadingTx = true;
|
||||
error = null;
|
||||
subscription: Subscription;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private bisqApiService: BisqApiService,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private stateService: StateService,
|
||||
private seoService: SeoService,
|
||||
private router: Router,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.subscription = this.route.paramMap.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.isLoading = true;
|
||||
this.isLoadingTx = true;
|
||||
this.error = null;
|
||||
document.body.scrollTo(0, 0);
|
||||
this.txId = params.get('id') || '';
|
||||
this.seoService.setTitle('Transaction: ' + this.txId, true);
|
||||
if (history.state.data) {
|
||||
return of(history.state.data);
|
||||
}
|
||||
return this.bisqApiService.getTransaction$(this.txId)
|
||||
.pipe(
|
||||
catchError((bisqTxError: HttpErrorResponse) => {
|
||||
if (bisqTxError.status === 404) {
|
||||
return this.electrsApiService.getTransaction$(this.txId)
|
||||
.pipe(
|
||||
map((tx) => {
|
||||
if (tx.status.confirmed) {
|
||||
this.error = {
|
||||
status: 200,
|
||||
statusText: 'Transaction is confirmed but not available in the Bisq database, please try reloading this page.'
|
||||
};
|
||||
return null;
|
||||
}
|
||||
return tx;
|
||||
}),
|
||||
catchError((txError: HttpErrorResponse) => {
|
||||
console.log(txError);
|
||||
this.error = txError;
|
||||
return of(null);
|
||||
})
|
||||
);
|
||||
}
|
||||
this.error = bisqTxError;
|
||||
return of(null);
|
||||
})
|
||||
);
|
||||
}),
|
||||
switchMap((tx) => {
|
||||
if (!tx) {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
if (tx.version) {
|
||||
this.router.navigate(['/tx/', this.txId], { state: { data: tx, bsqTx: true }});
|
||||
return of(null);
|
||||
}
|
||||
|
||||
this.bisqTx = tx;
|
||||
this.isLoading = false;
|
||||
|
||||
return this.electrsApiService.getTransaction$(this.txId);
|
||||
}),
|
||||
)
|
||||
.subscribe((tx) => {
|
||||
this.isLoadingTx = false;
|
||||
|
||||
if (!tx) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tx = tx;
|
||||
},
|
||||
(error) => {
|
||||
this.error = error;
|
||||
});
|
||||
|
||||
this.latestBlock$ = this.stateService.blocks$.pipe(map((([block]) => block)));
|
||||
|
||||
this.stateService.bsqPrice$
|
||||
.subscribe((bsqPrice) => {
|
||||
this.price = bsqPrice;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
<div class="container-xl">
|
||||
<h1 style="float: left;">Transactions</h1>
|
||||
<br>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<th style="width: 20%;">Transaction</th>
|
||||
<th class="d-none d-md-block" style="width: 20%;">Type</th>
|
||||
<th style="width: 20%;">Amount</th>
|
||||
<th style="width: 20%;">Confirmed</th>
|
||||
<th class="d-none d-md-block" style="width: 20%;">Height</th>
|
||||
</thead>
|
||||
<tbody *ngIf="!isLoading; else loadingTmpl">
|
||||
<tr *ngFor="let tx of transactions; trackBy: trackByFn">
|
||||
<td><a [routerLink]="['/tx/' | relativeUrl, tx.id]" [state]="{ data: tx }">{{ tx.id | slice : 0 : 8 }}</a></td>
|
||||
<td class="d-none d-md-block">
|
||||
<app-bisq-icon class="mr-1" [txType]="tx.txType"></app-bisq-icon>
|
||||
<span class="d-none d-md-inline"> {{ tx.txTypeDisplayString }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<app-bisq-icon class="d-inline d-md-none mr-1" [txType]="tx.txType"></app-bisq-icon>
|
||||
<ng-template [ngIf]="tx.txType === 'PAY_TRADE_FEE'" [ngIfElse]="defaultTxType">
|
||||
{{ tx.burntFee / 100 | number: '1.2-2' }}<span class="d-none d-md-inline"> BSQ</span>
|
||||
</ng-template>
|
||||
<ng-template #defaultTxType>
|
||||
{{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }}<span class="d-none d-md-inline"> BSQ</span>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td><app-time-since [time]="tx.time / 1000" [fastRender]="true"></app-time-since> ago</td>
|
||||
<td class="d-none d-md-block"><a [routerLink]="['/block/' | relativeUrl, tx.blockHash]" [state]="{ data: { blockHeight: tx.blockHeight } }">{{ tx.blockHeight }}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br>
|
||||
|
||||
<ngb-pagination [size]="paginationSize" [collectionSize]="totalCount" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true"></ngb-pagination>
|
||||
|
||||
</div>
|
||||
|
||||
<ng-template #loadingTmpl>
|
||||
<tr *ngFor="let i of loadingItems">
|
||||
<td *ngFor="let j of [1, 2, 3, 4, 5]"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</ng-template>
|
@ -0,0 +1,71 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { BisqTransaction, BisqOutput } from '../bisq.interfaces';
|
||||
import { Subject } from 'rxjs';
|
||||
import { switchMap, tap } from 'rxjs/operators';
|
||||
import { BisqApiService } from '../bisq-api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-transactions',
|
||||
templateUrl: './bisq-transactions.component.html',
|
||||
styleUrls: ['./bisq-transactions.component.scss']
|
||||
})
|
||||
export class BisqTransactionsComponent implements OnInit {
|
||||
transactions: BisqTransaction[];
|
||||
totalCount: number;
|
||||
page = 1;
|
||||
itemsPerPage: number;
|
||||
contentSpace = window.innerHeight - (165 + 75);
|
||||
fiveItemsPxSize = 250;
|
||||
isLoading = true;
|
||||
loadingItems: number[];
|
||||
pageSubject$ = new Subject<number>();
|
||||
|
||||
// @ts-ignore
|
||||
paginationSize: 'sm' | 'lg' = 'md';
|
||||
paginationMaxSize = 10;
|
||||
|
||||
constructor(
|
||||
private bisqApiService: BisqApiService,
|
||||
private seoService: SeoService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle('Transactions', true);
|
||||
|
||||
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
|
||||
this.loadingItems = Array(this.itemsPerPage);
|
||||
|
||||
if (document.body.clientWidth < 768) {
|
||||
this.paginationSize = 'sm';
|
||||
this.paginationMaxSize = 3;
|
||||
}
|
||||
|
||||
this.pageSubject$
|
||||
.pipe(
|
||||
tap(() => this.isLoading = true),
|
||||
switchMap((page) => this.bisqApiService.listTransactions$((page - 1) * this.itemsPerPage, this.itemsPerPage))
|
||||
)
|
||||
.subscribe((response) => {
|
||||
this.isLoading = false;
|
||||
this.transactions = response.body;
|
||||
this.totalCount = parseInt(response.headers.get('x-total-count'), 10);
|
||||
}, (error) => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
this.pageSubject$.next(1);
|
||||
}
|
||||
|
||||
pageChange(page: number) {
|
||||
this.pageSubject$.next(page);
|
||||
}
|
||||
|
||||
calculateTotalOutput(outputs: BisqOutput[]): number {
|
||||
return outputs.reduce((acc: number, output: BisqOutput) => acc + output.bsqAmount, 0);
|
||||
}
|
||||
|
||||
trackByFn(index: number) {
|
||||
return index;
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
<div class="header-bg box">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-borderless smaller-text table-xs" style="margin: 0;">
|
||||
<tbody>
|
||||
<ng-template ngFor let-input [ngForOf]="tx.inputs" [ngForTrackBy]="trackByIndexFn">
|
||||
<tr *ngIf="input.isVerified">
|
||||
<td class="arrow-td">
|
||||
<ng-template [ngIf]="input.spendingTxId === null" [ngIfElse]="hasPreoutput">
|
||||
<i class="arrow grey"></i>
|
||||
</ng-template>
|
||||
<ng-template #hasPreoutput>
|
||||
<a [routerLink]="['/tx/' | relativeUrl, input.spendingTxId]">
|
||||
<i class="arrow red"></i>
|
||||
</a>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td>
|
||||
<a [routerLink]="['/address/' | relativeUrl, 'B' + input.address]" title="B{{ input.address }}">
|
||||
<span class="d-block d-lg-none">B{{ input.address | shortenString : 16 }}</span>
|
||||
<span class="d-none d-lg-block">B{{ input.address | shortenString : 35 }}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right nowrap">
|
||||
<app-bsq-amount [bsq]="input.bsqAmount"></app-bsq-amount>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="w-100 d-block d-md-none"></div>
|
||||
<div class="col mobile-bottomcol">
|
||||
<table class="table table-borderless smaller-text table-xs" style="margin: 0;">
|
||||
<tbody>
|
||||
<ng-template ngFor let-output [ngForOf]="tx.outputs" [ngForTrackBy]="trackByIndexFn">
|
||||
<tr *ngIf="output.isVerified && output.opReturn === undefined">
|
||||
<td>
|
||||
<a [routerLink]="['/address/' | relativeUrl, 'B' + output.address]" title="B{{ output.address }}">
|
||||
<span class="d-block d-lg-none">B{{ output.address | shortenString : 16 }}</span>
|
||||
<span class="d-none d-lg-block">B{{ output.address | shortenString : 35 }}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right nowrap">
|
||||
<app-bsq-amount [bsq]="output.bsqAmount"></app-bsq-amount>
|
||||
</td>
|
||||
<td class="pl-1 arrow-td">
|
||||
<i *ngIf="!output.spentInfo; else spent" class="arrow green"></i>
|
||||
<ng-template #spent>
|
||||
<a [routerLink]="['/tx/' | relativeUrl, output.spentInfo.txId]"><i class="arrow red"></i></a>
|
||||
</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="float-left mt-2-5" *ngIf="showConfirmations && tx.burntFee">
|
||||
Burnt: {{ tx.burntFee / 100 | number: '1.2-2' }} BSQ (<app-bsq-amount [bsq]="tx.burntFee" [forceFiat]="true" [green]="true"></app-bsq-amount>)
|
||||
</div>
|
||||
|
||||
<div class="float-right">
|
||||
<span *ngIf="showConfirmations && latestBlock$ | async as latestBlock">
|
||||
<button type="button" class="btn btn-sm btn-success mt-2">{{ latestBlock.height - tx.blockHeight + 1 }} confirmation<ng-container *ngIf="latestBlock.height - tx.blockHeight + 1 > 1">s</ng-container></button>
|
||||
|
||||
</span>
|
||||
<button type="button" class="btn btn-sm btn-primary mt-2" (click)="switchCurrency()">
|
||||
<app-bsq-amount [bsq]="totalOutput"></app-bsq-amount>
|
||||
</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
@ -0,0 +1,84 @@
|
||||
.arrow-td {
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
display: inline-block!important;
|
||||
position: relative;
|
||||
width: 14px;
|
||||
height: 22px;
|
||||
box-sizing: content-box
|
||||
}
|
||||
|
||||
.arrow:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
margin: auto;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: calc(-1*30px/3);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 6.66px solid transparent;
|
||||
border-bottom: 6.66px solid transparent
|
||||
}
|
||||
|
||||
.arrow:after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
margin: auto;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: calc(30px/6);
|
||||
width: calc(30px/3);
|
||||
height: calc(20px/3);
|
||||
background: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.arrow.green:before {
|
||||
border-left: 10px solid #28a745;
|
||||
}
|
||||
.arrow.green:after {
|
||||
background-color:#28a745;
|
||||
}
|
||||
|
||||
.arrow.red:before {
|
||||
border-left: 10px solid #dc3545;
|
||||
}
|
||||
.arrow.red:after {
|
||||
background-color:#dc3545;
|
||||
}
|
||||
|
||||
.arrow.grey:before {
|
||||
border-left: 10px solid #6c757d;
|
||||
}
|
||||
|
||||
.arrow.grey:after {
|
||||
background-color:#6c757d;
|
||||
}
|
||||
|
||||
.scriptmessage {
|
||||
max-width: 280px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.scriptmessage.longer {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.mobile-bottomcol {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.scriptmessage {
|
||||
max-width: 90px !important;
|
||||
}
|
||||
.scriptmessage.longer {
|
||||
max-width: 280px !important;
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
|
||||
import { BisqTransaction } from 'src/app/bisq/bisq.interfaces';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Block } from 'src/app/interfaces/electrs.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-transfers',
|
||||
templateUrl: './bisq-transfers.component.html',
|
||||
styleUrls: ['./bisq-transfers.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class BisqTransfersComponent implements OnInit, OnChanges {
|
||||
@Input() tx: BisqTransaction;
|
||||
@Input() showConfirmations = false;
|
||||
|
||||
totalOutput: number;
|
||||
latestBlock$: Observable<Block>;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
|
||||
trackByIndexFn(index: number) {
|
||||
return index;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.latestBlock$ = this.stateService.blocks$.pipe(map(([block]) => block));
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.totalOutput = this.tx.outputs.filter((output) => output.isVerified).reduce((acc, output) => acc + output.bsqAmount, 0);;
|
||||
}
|
||||
|
||||
switchCurrency() {
|
||||
const oldvalue = !this.stateService.viewFiat$.value;
|
||||
this.stateService.viewFiat$.next(oldvalue);
|
||||
}
|
||||
|
||||
}
|
82
frontend/src/app/bisq/bisq.interfaces.ts
Normal file
82
frontend/src/app/bisq/bisq.interfaces.ts
Normal file
@ -0,0 +1,82 @@
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export interface BisqOutput {
|
||||
txVersion: string;
|
||||
txId: string;
|
||||
index: number;
|
||||
bsqAmount: number;
|
||||
btcAmount: number;
|
||||
height: number;
|
||||
isVerified: boolean;
|
||||
burntFee: number;
|
||||
invalidatedBsq: number;
|
||||
address: string;
|
||||
scriptPubKey: BisqScriptPubKey;
|
||||
spentInfo?: SpentInfo;
|
||||
time: any;
|
||||
txType: string;
|
||||
txTypeDisplayString: string;
|
||||
txOutputType: string;
|
||||
txOutputTypeDisplayString: string;
|
||||
lockTime: number;
|
||||
isUnspent: boolean;
|
||||
opReturn?: string;
|
||||
}
|
||||
|
||||
export interface BisqStats {
|
||||
minted: number;
|
||||
burnt: number;
|
||||
addresses: number;
|
||||
unspent_txos: number;
|
||||
spent_txos: number;
|
||||
}
|
||||
|
||||
interface BisqScriptPubKey {
|
||||
addresses: string[];
|
||||
asm: string;
|
||||
hex: string;
|
||||
reqSigs: number;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface SpentInfo {
|
||||
height: number;
|
||||
inputIndex: number;
|
||||
txId: string;
|
||||
}
|
60
frontend/src/app/bisq/bisq.module.ts
Normal file
60
frontend/src/app/bisq/bisq.module.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BisqRoutingModule } from './bisq.routing.module';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { BisqTransactionsComponent } from './bisq-transactions/bisq-transactions.component';
|
||||
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.component';
|
||||
import { BisqBlockComponent } from './bisq-block/bisq-block.component';
|
||||
import { BisqIconComponent } from './bisq-icon/bisq-icon.component';
|
||||
import { BisqTransactionDetailsComponent } from './bisq-transaction-details/bisq-transaction-details.component';
|
||||
import { BisqTransfersComponent } from './bisq-transfers/bisq-transfers.component';
|
||||
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
||||
import { faLeaf, faQuestion, faExclamationTriangle, faRocket, faRetweet, faFileAlt, faMoneyBill,
|
||||
faEye, faEyeSlash, faLock, faLockOpen } from '@fortawesome/free-solid-svg-icons';
|
||||
import { BisqBlocksComponent } from './bisq-blocks/bisq-blocks.component';
|
||||
import { BisqExplorerComponent } from './bisq-explorer/bisq-explorer.component';
|
||||
import { BisqApiService } from './bisq-api.service';
|
||||
import { BisqAddressComponent } from './bisq-address/bisq-address.component';
|
||||
import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
|
||||
import { BsqAmountComponent } from './bsq-amount/bsq-amount.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
BisqTransactionsComponent,
|
||||
BisqTransactionComponent,
|
||||
BisqBlockComponent,
|
||||
BisqTransactionComponent,
|
||||
BisqIconComponent,
|
||||
BisqTransactionDetailsComponent,
|
||||
BisqTransfersComponent,
|
||||
BisqBlocksComponent,
|
||||
BisqExplorerComponent,
|
||||
BisqAddressComponent,
|
||||
BisqStatsComponent,
|
||||
BsqAmountComponent,
|
||||
],
|
||||
imports: [
|
||||
BisqRoutingModule,
|
||||
SharedModule,
|
||||
NgbPaginationModule,
|
||||
FontAwesomeModule,
|
||||
],
|
||||
providers: [
|
||||
BisqApiService,
|
||||
]
|
||||
})
|
||||
export class BisqModule {
|
||||
constructor(library: FaIconLibrary) {
|
||||
library.addIcons(faQuestion);
|
||||
library.addIcons(faExclamationTriangle);
|
||||
library.addIcons(faRocket);
|
||||
library.addIcons(faRetweet);
|
||||
library.addIcons(faLeaf);
|
||||
library.addIcons(faFileAlt);
|
||||
library.addIcons(faMoneyBill);
|
||||
library.addIcons(faEye);
|
||||
library.addIcons(faEyeSlash);
|
||||
library.addIcons(faLock);
|
||||
library.addIcons(faLockOpen);
|
||||
}
|
||||
}
|
59
frontend/src/app/bisq/bisq.routing.module.ts
Normal file
59
frontend/src/app/bisq/bisq.routing.module.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AboutComponent } from '../components/about/about.component';
|
||||
import { AddressComponent } from '../components/address/address.component';
|
||||
import { BisqTransactionsComponent } from './bisq-transactions/bisq-transactions.component';
|
||||
import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.component';
|
||||
import { BisqBlockComponent } from './bisq-block/bisq-block.component';
|
||||
import { BisqBlocksComponent } from './bisq-blocks/bisq-blocks.component';
|
||||
import { BisqExplorerComponent } from './bisq-explorer/bisq-explorer.component';
|
||||
import { BisqAddressComponent } from './bisq-address/bisq-address.component';
|
||||
import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: BisqExplorerComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: BisqTransactionsComponent
|
||||
},
|
||||
{
|
||||
path: 'tx/:id',
|
||||
component: BisqTransactionComponent
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
children: [],
|
||||
component: BisqBlocksComponent
|
||||
},
|
||||
{
|
||||
path: 'block/:id',
|
||||
component: BisqBlockComponent,
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
component: BisqAddressComponent,
|
||||
},
|
||||
{
|
||||
path: 'stats',
|
||||
component: BisqStatsComponent,
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
component: AboutComponent,
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class BisqRoutingModule { }
|
@ -0,0 +1,6 @@
|
||||
<ng-container *ngIf="(forceFiat || (viewFiat$ | async)) && (conversions$ | async) as conversions; else viewFiatVin">
|
||||
<span [class.green-color]="green">{{ conversions.USD * bsq / 100 * (bsqPrice$ | async) / 100000000 | currency:'USD':'symbol':'1.2-2' }}</span>
|
||||
</ng-container>
|
||||
<ng-template #viewFiatVin>
|
||||
{{ bsq / 100 | number : digitsInfo }} BSQ
|
||||
</ng-template>
|
@ -0,0 +1,3 @@
|
||||
.green-color {
|
||||
color: #3bcc49;
|
||||
}
|
30
frontend/src/app/bisq/bsq-amount/bsq-amount.component.ts
Normal file
30
frontend/src/app/bisq/bsq-amount/bsq-amount.component.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bsq-amount',
|
||||
templateUrl: './bsq-amount.component.html',
|
||||
styleUrls: ['./bsq-amount.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BsqAmountComponent implements OnInit {
|
||||
conversions$: Observable<any>;
|
||||
viewFiat$: Observable<boolean>;
|
||||
bsqPrice$: Observable<number>;
|
||||
|
||||
@Input() bsq: number;
|
||||
@Input() digitsInfo = '1.2-2';
|
||||
@Input() forceFiat = false;
|
||||
@Input() green = false;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.viewFiat$ = this.stateService.viewFiat$.asObservable();
|
||||
this.conversions$ = this.stateService.conversions$.asObservable();
|
||||
this.bsqPrice$ = this.stateService.bsqPrice$;
|
||||
}
|
||||
}
|
@ -4,15 +4,14 @@
|
||||
<img src="./resources/mempool-tube.png" width="63" height="63" />
|
||||
<br /><br />
|
||||
|
||||
<h1>Contributors</h1>
|
||||
<h2>Contributors</h2>
|
||||
|
||||
<p>Development <a href="https://twitter.com/softsimon_">@softsimon_</a>
|
||||
<br />Operations <a href="https://twitter.com/wiz">@wiz</a>
|
||||
<br />Design <a href="https://instagram.com/markjborg">@markjborg</a>
|
||||
|
||||
<br><br>
|
||||
|
||||
<h2>Github</h2>
|
||||
<h2>Open source</h2>
|
||||
|
||||
<a target="_blank" class="b2812e30 f2874b88 fw6 mb3 mt2 truncate black-80 f4 link" rel="noopener noreferrer nofollow" href="https://github.com/mempool/mempool">
|
||||
<span class="_9e13d83d dib v-mid">
|
||||
@ -29,54 +28,88 @@
|
||||
<br><br>
|
||||
|
||||
<div class="text-center">
|
||||
<h2>HTTP API</h2>
|
||||
<h2>API</h2>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td>Fee API</td>
|
||||
<td>
|
||||
<div class="mx-auto">
|
||||
<input class="form-control" type="text" value="https://mempool.space/api/v1/fees/recommended" readonly>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Mempool blocks</td>
|
||||
<td>
|
||||
<div class="mx-auto">
|
||||
<input class="form-control" type="text" value="https://mempool.space/api/v1/fees/mempool-blocks" readonly>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-tabs">
|
||||
<li [ngbNavItem]="1">
|
||||
<a ngbNavLink>Mainnet</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<br><br>
|
||||
|
||||
<div class="text-center">
|
||||
<h2>WebSocket API</h2>
|
||||
</div>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th style="border-top: 0;">Endpoint</th>
|
||||
<th style="border-top: 0;">Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nowrap"><a href="/api/v1/fees/recommended" target="_blank">GET /api/v1/fees/recommended</a></td>
|
||||
<td>Recommended fees</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nowrap"><a href="/api/v1/fees/mempool-blocks" target="_blank">GET /api/v1/fees/mempool-blocks</a></td>
|
||||
<td>The current mempool blocks</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nowrap">wss://{{ hostname }}/api/v1/ws</td>
|
||||
<td>
|
||||
<span class="text-small">
|
||||
Default push: <span class="code">{{ '{' }} action: 'want', data: ['blocks', ...] {{ '}' }}</span>
|
||||
to express what you want pushed. Available: 'blocks', 'mempool-blocks', 'live-2h-chart' and 'stats'.
|
||||
</span>
|
||||
<br><br>
|
||||
<span class="text-small">
|
||||
Push transactions related to address: <span class="code">{{ '{' }} 'track-address': '3PbJ...bF9B' {{ '}' }}</span>
|
||||
to receive all new transactions containing that address as input or output. Returns an array of transactions. 'address-transactions' for new mempool transactions and 'block-transactions' for new block confirmed transactions.
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td>
|
||||
<span class="text-small">
|
||||
Default push: <span class="code">{{ '{' }} action: 'want', data: ['blocks', ...] {{ '}' }}</span>
|
||||
to express what you want pushed. Available: 'blocks', 'mempool-blocks', 'live-2h-chart' and 'stats'.
|
||||
</span>
|
||||
<br><br>
|
||||
<span class="text-small">
|
||||
Push transactions related to address: <span class="code">{{ '{' }} 'track-address': '3PbJ...bF9B' {{ '}' }}</span>
|
||||
to receive all new transactions containing that address as input or output. Returns an array of transactions. 'address-transactions' for new mempool transactions and 'block-transactions' for new block confirmed transactions.
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 0;">
|
||||
<input class="form-control" type="text" value="wss://mempool.space/api/v1/ws" readonly>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="2">
|
||||
<a ngbNavLink>Bisq</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th style="border-top: 0;">Endpoint</th>
|
||||
<th style="border-top: 0;">Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nowrap"><a href="/bisq/api/stats" target="_blank">GET /bisq/api/stats</a></td>
|
||||
<td>Stats</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nowrap"><a href="/bisq/api/tx/4b5417ec5ab6112bedf539c3b4f5a806ed539542d8b717e1c4470aa3180edce5" target="_blank">GET /bisq/api/tx/:txId</a></td>
|
||||
<td>Transaction</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nowrap"><a href="/bisq/api/txs/0/25" target="_blank">GET /bisq/api/txs/:index/:length</a></td>
|
||||
<td>Transactions</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nowrap"><a href="/bisq/api/block/000000000000000000079aa6bfa46eb8fc20474e8673d6e8a123b211236bf82d" target="_blank">GET /bisq/api/block/:hash</a></td>
|
||||
<td>Block</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nowrap"><a href="/bisq/api/blocks/0/25" target="_blank">GET /bisq/api/blocks/:index/:length</a></td>
|
||||
<td>Blocks</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nowrap"><a href="/bisq/api/blocks/tip/height" target="_blank">GET /bisq/api/blocks/tip/height</a></td>
|
||||
<td>Latest block height</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nowrap"><a href="/bisq/api/address/B1DgwRN92rdQ9xpEVCdXRfgeqGw9X4YtrZz" target="_blank">GET /bisq/api/address/:address</a></td>
|
||||
<td>Address</td>
|
||||
</tr>
|
||||
</table>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
||||
|
||||
<br> <br>
|
||||
|
||||
|
@ -9,4 +9,8 @@
|
||||
|
||||
tr {
|
||||
white-space: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about',
|
||||
@ -8,15 +9,24 @@ import { SeoService } from 'src/app/services/seo.service';
|
||||
styleUrls: ['./about.component.scss']
|
||||
})
|
||||
export class AboutComponent implements OnInit {
|
||||
active = 1;
|
||||
hostname = document.location.hostname;
|
||||
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private seoService: SeoService,
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.seoService.setTitle('Contributors');
|
||||
this.websocketService.want(['blocks']);
|
||||
if (this.stateService.network === 'bisq') {
|
||||
this.active = 2;
|
||||
}
|
||||
if (document.location.port !== '') {
|
||||
this.hostname = this.hostname + ':' + document.location.port;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -157,7 +157,7 @@
|
||||
<ng-template [ngIf]="error">
|
||||
<div class="text-center">
|
||||
Error loading block data.
|
||||
<br>
|
||||
<br><br>
|
||||
<i>{{ error.error }}</i>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
@ -4,7 +4,7 @@ import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { switchMap, tap, debounceTime, catchError } from 'rxjs/operators';
|
||||
import { Block, Transaction, Vout } from '../../interfaces/electrs.interface';
|
||||
import { of } from 'rxjs';
|
||||
import { of, Subscription } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { env } from 'src/app/app.constants';
|
||||
@ -25,6 +25,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
isLoadingTransactions = true;
|
||||
error: any;
|
||||
blockSubsidy: number;
|
||||
subscription: Subscription;
|
||||
fees: number;
|
||||
paginationMaxSize: number;
|
||||
page = 1;
|
||||
@ -43,7 +44,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.paginationMaxSize = window.matchMedia('(max-width: 700px)').matches ? 3 : 5;
|
||||
this.network = this.stateService.network;
|
||||
|
||||
this.route.paramMap
|
||||
this.subscription = this.route.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
const blockHash: string = params.get('id') || '';
|
||||
@ -129,6 +130,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnDestroy() {
|
||||
this.stateService.markBlock$.next({});
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
setBlockSubsidy() {
|
||||
|
@ -26,6 +26,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
||||
|
||||
gradientColors = {
|
||||
'': ['#9339f4', '#105fb0'],
|
||||
bisq: ['#9339f4', '#105fb0'],
|
||||
liquid: ['#116761', '#183550'],
|
||||
testnet: ['#1d486f', '#183550'],
|
||||
};
|
||||
|
@ -17,6 +17,5 @@ export class BlockchainComponent implements OnInit {
|
||||
|
||||
ngOnInit() {
|
||||
this.stateService.blocks$.subscribe(() => this.isLoading = false);
|
||||
this.stateService.networkChanged$.subscribe(() => this.isLoading = true);
|
||||
}
|
||||
}
|
||||
|
@ -1,36 +1,52 @@
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
||||
<a class="navbar-brand" routerLink="/" style="position: relative;">
|
||||
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
|
||||
<img src="./resources/mempool-logo.png" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState === 2 ? 1 : 0.5 }">
|
||||
<div class="badge badge-warning connection-badge" *ngIf="connectionState === 0">Offline</div>
|
||||
<div class="badge badge-warning connection-badge" style="left: 30px;" *ngIf="connectionState === 1">Reconnecting...</div>
|
||||
</a>
|
||||
|
||||
<div class="btn-group" style="margin-right: 16px;" *ngIf="env.TESTNET_ENABLED || env.LIQUID_ENABLED">
|
||||
<button type="button" (click)="networkDropdownHidden = !networkDropdownHidden" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
|
||||
<div ngbDropdown display="dynamic" style="margin-right: 16px;" *ngIf="env.TESTNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED">
|
||||
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
|
||||
<img src="./resources/{{ network === '' ? 'bitcoin' : network }}-logo.png" style="width: 25px; height: 25px;" class="mr-1">
|
||||
</button>
|
||||
<div class="dropdown-menu" [class.d-block]="!networkDropdownHidden">
|
||||
<a class="dropdown-item mainnet" [class.active]="network === ''" routerLink="/"><img src="./resources/bitcoin-logo.png" style="width: 35.5px;"> Mainnet</a>
|
||||
<a *ngIf="env.LIQUID_ENABLED" class="dropdown-item liquid" [class.active]="network === 'liquid'" routerLink="/liquid"><img src="./resources/liquid-logo.png" style="width: 35.5px;"> Liquid</a>
|
||||
<a *ngIf="env.TESTNET_ENABLED" class="dropdown-item testnet" [class.active]="network === 'testnet'" routerLink="/testnet"><img src="./resources/testnet-logo.png" style="width: 35.5px;"> Testnet</a>
|
||||
<div ngbDropdownMenu>
|
||||
<button ngbDropdownItem class="mainnet" routerLink="/"><img src="./resources/bitcoin-logo.png" style="width: 30px;" class="mr-1"> Mainnet</button>
|
||||
<button ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network === 'testnet'" routerLink="/testnet"><img src="./resources/testnet-logo.png" style="width: 30px;" class="mr-1"> Testnet</button>
|
||||
<h6 *ngIf="env.LIQUID_ENABLED || env.BISQ_ENABLED" class="dropdown-header">Layer 2 Networks</h6>
|
||||
<button ngbDropdownItem *ngIf="env.BISQ_ENABLED" class="mainnet" [class.active]="network === 'bisq'" routerLink="/bisq"><img src="./resources/bisq-logo.png" style="width: 30px;" class="mr-1"> Bisq</button>
|
||||
<button ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid" [class.active]="network === 'liquid'" routerLink="/liquid"><img src="./resources/liquid-logo.png" style="width: 30px;" class="mr-1"> Liquid</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="navbar-toggler" type="button" (click)="collapse()" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<button class="navbar-toggler" type="button" (click)="collapse()">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="navbar-collapse collapse" id="navbarCollapse" [ngClass]="{'show': navCollapsed}">
|
||||
<ul class="navbar-nav mr-auto {{ network }}">
|
||||
<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" [routerLink]="['/graphs' | relativeUrl]" (click)="collapse()">Graphs</a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" [routerLink]="['/tv' | relativeUrl]" (click)="collapse()">TV view <img src="./resources/expand.png" width="15"/></a>
|
||||
</li>
|
||||
<ng-template [ngIf]="network === 'bisq'" [ngIfElse]="notBisq">
|
||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
|
||||
<a class="nav-link" [routerLink]="['/bisq']" (click)="collapse()">Transactions</a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" [routerLink]="['/bisq/blocks']" (click)="collapse()">Blocks</a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" [routerLink]="['/bisq/stats']" (click)="collapse()">Stats</a>
|
||||
</li>
|
||||
</ng-template>
|
||||
<ng-template #notBisq>
|
||||
<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" [routerLink]="['/graphs' | relativeUrl]" (click)="collapse()">Graphs</a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" [routerLink]="['/tv' | relativeUrl]" (click)="collapse()">TV view <img src="./resources/expand.png" width="15"/></a>
|
||||
</li>
|
||||
</ng-template>
|
||||
<li *ngIf="network === 'liquid'" class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" [routerLink]="['/assets' | relativeUrl]" (click)="collapse()">Assets</a>
|
||||
<a class="nav-link" [routerLink]="['liquid/assets']" (click)="collapse()">Assets</a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" [routerLink]="['/about' | relativeUrl]" (click)="collapse()">About</a>
|
||||
@ -45,6 +61,10 @@
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
<br><br><br>
|
||||
<br>
|
||||
|
||||
<app-footer></app-footer>
|
||||
<ng-template [ngIf]="network !== 'bisq'">
|
||||
<br><br>
|
||||
|
||||
<app-footer></app-footer>
|
||||
</ng-template>
|
||||
|
@ -47,3 +47,16 @@ nav {
|
||||
.testnet.active {
|
||||
background-color: #1d486f;
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
border-top: 1px solid #121420;
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
vertical-align: 0.1em;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items:center;
|
||||
}
|
||||
|
@ -15,19 +15,10 @@ export class MasterPageComponent implements OnInit {
|
||||
navCollapsed = false;
|
||||
connectionState = 2;
|
||||
|
||||
networkDropdownHidden = true;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
|
||||
@HostListener('document:click', ['$event'])
|
||||
documentClick(event: any): void {
|
||||
if (!event.target.classList.contains('dropdown-toggle')) {
|
||||
this.networkDropdownHidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.stateService.connectionState$
|
||||
.subscribe((state) => {
|
||||
@ -37,14 +28,6 @@ export class MasterPageComponent implements OnInit {
|
||||
this.stateService.networkChanged$
|
||||
.subscribe((network) => {
|
||||
this.network = network;
|
||||
|
||||
if (network === 'testnet') {
|
||||
this.tvViewRoute = '/testnet-tv';
|
||||
} else if (network === 'liquid') {
|
||||
this.tvViewRoute = '/liquid-tv';
|
||||
} else {
|
||||
this.tvViewRoute = '/tv';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Component, OnInit, Input, Inject, LOCALE_ID, ChangeDetectionStrategy, OnChanges } from '@angular/core';
|
||||
import { formatDate } from '@angular/common';
|
||||
import { VbytesPipe } from 'src/app/pipes/bytes-pipe/vbytes.pipe';
|
||||
import { VbytesPipe } from 'src/app/shared/pipes/bytes-pipe/vbytes.pipe';
|
||||
import * as Chartist from 'chartist';
|
||||
import { OptimizedMempoolStats } from 'src/app/interfaces/node-api.interface';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
|
@ -35,7 +35,7 @@ export class QrcodeComponent implements AfterViewInit {
|
||||
address.toUpperCase();
|
||||
}
|
||||
|
||||
QRCode.toCanvas(this.canvas.nativeElement, 'bitcoin:' + address, opts, (error: any) => {
|
||||
QRCode.toCanvas(this.canvas.nativeElement, address, opts, (error: any) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
@ -51,7 +51,12 @@
|
||||
<td>After <app-timespan [time]="tx.status.block_time - transactionTime"></app-timespan></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-container *ngTemplateOutlet="features"></ng-container>
|
||||
<tr *ngIf="network !== 'liquid'">
|
||||
<td class="td-width">Features</td>
|
||||
<td>
|
||||
<app-tx-features [tx]="tx"></app-tx-features>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -68,9 +73,7 @@
|
||||
<td>
|
||||
{{ tx.fee / (tx.weight / 4) | number : '1.1-1' }} sat/vB
|
||||
|
||||
<span *ngIf="feeRating === 1" class="badge badge-success">Optimal</span>
|
||||
<span *ngIf="feeRating === 2" class="badge badge-warning" title="Only ~{{ medianFeeNeeded }} sat/vB was needed to get into this block">Overpaid {{ overpaidTimes }}x</span>
|
||||
<span *ngIf="feeRating === 3" class="badge badge-danger" title="Only ~{{ medianFeeNeeded }} sat/vB was needed to get into this block">Overpaid {{ overpaidTimes }}x</span>
|
||||
<app-tx-fee-rating [tx]="tx"></app-tx-fee-rating>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
@ -124,7 +127,12 @@
|
||||
</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
<ng-container *ngTemplateOutlet="features"></ng-container>
|
||||
<tr *ngIf="network !== 'liquid'">
|
||||
<td class="td-width">Features</td>
|
||||
<td>
|
||||
<app-tx-features [tx]="tx"></app-tx-features>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -244,7 +252,7 @@
|
||||
|
||||
<ng-template [ngIf]="error">
|
||||
|
||||
<div class="text-center" *ngIf="waitingForTransaction">
|
||||
<div class="text-center" *ngIf="waitingForTransaction; else errorTemplate">
|
||||
<h3>Transaction not found.</h3>
|
||||
<h5>Waiting for it to appear in the mempool...</h5>
|
||||
<div class="spinner-border text-light mt-2"></div>
|
||||
@ -260,15 +268,3 @@
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<ng-template #features>
|
||||
<tr *ngIf="network !== 'liquid'">
|
||||
<td class="td-width">Features</td>
|
||||
<td>
|
||||
<span *ngIf="segwitGains.realizedGains && !segwitGains.potentialBech32Gains" class="badge badge-success mr-1" ngbTooltip="This transaction saved {{ segwitGains.realizedGains * 100 | number: '1.0-0' }}% on fees by using native SegWit-Bech32" placement="bottom">SegWit</span>
|
||||
<span *ngIf="segwitGains.realizedGains && segwitGains.potentialBech32Gains" class="badge badge-warning mr-1" ngbTooltip="This transaction saved {{ segwitGains.realizedGains * 100 | number: '1.0-0' }}% on fees by using SegWit and could save {{ segwitGains.potentialBech32Gains * 100 | number : '1.0-0' }}% more by fully upgrading to native SegWit-Bech32" placement="bottom">SegWit</span>
|
||||
<span *ngIf="segwitGains.potentialP2shGains" class="badge badge-danger mr-1" ngbTooltip="This transaction could save {{ segwitGains.potentialBech32Gains * 100 | number : '1.0-0' }}% on fees by upgrading to native SegWit-Bech32 or {{ segwitGains.potentialP2shGains * 100 | number: '1.0-0' }}% by upgrading to SegWit-P2SH" placement="bottom"><del>SegWit</del></span>
|
||||
<span *ngIf="isRbfTransaction" class="badge badge-success" ngbTooltip="This transaction support Replace-By-Fee (RBF) allowing fee bumping" placement="bottom">RBF</span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
@ -9,7 +9,7 @@ import { WebsocketService } from '../../services/websocket.service';
|
||||
import { AudioService } from 'src/app/services/audio.service';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { calcSegwitFeeGains } from 'src/app/bitcoin.utils';
|
||||
import { BisqTransaction } from 'src/app/bisq/bisq.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'app-transaction',
|
||||
@ -20,9 +20,6 @@ export class TransactionComponent implements OnInit, OnDestroy {
|
||||
network = '';
|
||||
tx: Transaction;
|
||||
txId: string;
|
||||
feeRating: number;
|
||||
overpaidTimes: number;
|
||||
medianFeeNeeded: number;
|
||||
txInBlockIndex: number;
|
||||
isLoadingTx = true;
|
||||
error: any = undefined;
|
||||
@ -30,12 +27,6 @@ export class TransactionComponent implements OnInit, OnDestroy {
|
||||
latestBlock: Block;
|
||||
transactionTime = -1;
|
||||
subscription: Subscription;
|
||||
segwitGains = {
|
||||
realizedGains: 0,
|
||||
potentialBech32Gains: 0,
|
||||
potentialP2shGains: 0,
|
||||
};
|
||||
isRbfTransaction: boolean;
|
||||
rbfTransaction: undefined | Transaction;
|
||||
|
||||
constructor(
|
||||
@ -87,8 +78,6 @@ export class TransactionComponent implements OnInit, OnDestroy {
|
||||
this.error = undefined;
|
||||
this.waitingForTransaction = false;
|
||||
this.setMempoolBlocksSubscription();
|
||||
this.segwitGains = calcSegwitFeeGains(tx);
|
||||
this.isRbfTransaction = tx.vin.some((v) => v.sequence < 0xfffffffe);
|
||||
|
||||
if (!tx.status.confirmed) {
|
||||
this.websocketService.startTrackTransaction(tx.txid);
|
||||
@ -98,8 +87,6 @@ export class TransactionComponent implements OnInit, OnDestroy {
|
||||
} else {
|
||||
this.getTransactionTime();
|
||||
}
|
||||
} else {
|
||||
this.findBlockAndSetFeeRating();
|
||||
}
|
||||
if (this.tx.status.confirmed) {
|
||||
this.stateService.markBlock$.next({ blockHeight: tx.status.block_height });
|
||||
@ -125,7 +112,6 @@ export class TransactionComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
this.stateService.markBlock$.next({ blockHeight: block.height });
|
||||
this.audioService.playSound('magic');
|
||||
this.findBlockAndSetFeeRating();
|
||||
}
|
||||
});
|
||||
|
||||
@ -169,38 +155,9 @@ export class TransactionComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
findBlockAndSetFeeRating() {
|
||||
this.stateService.blocks$
|
||||
.pipe(
|
||||
filter(([block]) => block.height === this.tx.status.block_height),
|
||||
take(1)
|
||||
)
|
||||
.subscribe(([block]) => {
|
||||
const feePervByte = this.tx.fee / (this.tx.weight / 4);
|
||||
this.medianFeeNeeded = Math.round(block.feeRange[Math.round(block.feeRange.length * 0.5)]);
|
||||
|
||||
// Block not filled
|
||||
if (block.weight < 4000000 * 0.95) {
|
||||
this.medianFeeNeeded = 1;
|
||||
}
|
||||
|
||||
this.overpaidTimes = Math.round(feePervByte / this.medianFeeNeeded);
|
||||
|
||||
if (feePervByte <= this.medianFeeNeeded || this.overpaidTimes < 2) {
|
||||
this.feeRating = 1;
|
||||
} else {
|
||||
this.feeRating = 2;
|
||||
if (this.overpaidTimes > 10) {
|
||||
this.feeRating = 3;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resetTransaction() {
|
||||
this.error = undefined;
|
||||
this.tx = null;
|
||||
this.feeRating = undefined;
|
||||
this.waitingForTransaction = false;
|
||||
this.isLoadingTx = true;
|
||||
this.rbfTransaction = undefined;
|
||||
|
@ -17,7 +17,7 @@
|
||||
<div class="col">
|
||||
<table class="table table-borderless smaller-text table-xs" style="margin: 0;">
|
||||
<tbody>
|
||||
<tr *ngFor="let vin of getFilteredTxVin(tx)">
|
||||
<tr *ngFor="let vin of getFilteredTxVin(tx); trackBy: trackByIndexFn">
|
||||
<td class="arrow-td">
|
||||
<ng-template [ngIf]="vin.prevout === null && !vin.is_pegin" [ngIfElse]="hasPrevout">
|
||||
<i class="arrow grey"></i>
|
||||
@ -73,7 +73,7 @@
|
||||
<div class="col mobile-bottomcol">
|
||||
<table class="table table-borderless smaller-text table-xs" style="margin: 0;">
|
||||
<tbody>
|
||||
<tr *ngFor="let vout of getFilteredTxVout(tx); let vindex = index;">
|
||||
<tr *ngFor="let vout of getFilteredTxVout(tx); let vindex = index; trackBy: trackByIndexFn">
|
||||
<td>
|
||||
<a *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
|
||||
<span class="d-block d-lg-none">{{ vout.scriptpubkey_address | shortenString : 16 }}</span>
|
||||
|
@ -109,4 +109,8 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
getFilteredTxVout(tx: Transaction) {
|
||||
return tx.vout.slice(0, tx['@voutLength']);
|
||||
}
|
||||
|
||||
trackByIndexFn(index: number) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,4 @@
|
||||
<span *ngIf="segwitGains.realizedGains && !segwitGains.potentialBech32Gains" class="badge badge-success mr-1" ngbTooltip="This transaction saved {{ segwitGains.realizedGains * 100 | number: '1.0-0' }}% on fees by using native SegWit-Bech32" placement="bottom">SegWit</span>
|
||||
<span *ngIf="segwitGains.realizedGains && segwitGains.potentialBech32Gains" class="badge badge-warning mr-1" ngbTooltip="This transaction saved {{ segwitGains.realizedGains * 100 | number: '1.0-0' }}% on fees by using SegWit and could save {{ segwitGains.potentialBech32Gains * 100 | number : '1.0-0' }}% more by fully upgrading to native SegWit-Bech32" placement="bottom">SegWit</span>
|
||||
<span *ngIf="segwitGains.potentialP2shGains" class="badge badge-danger mr-1" ngbTooltip="This transaction could save {{ segwitGains.potentialBech32Gains * 100 | number : '1.0-0' }}% on fees by upgrading to native SegWit-Bech32 or {{ segwitGains.potentialP2shGains * 100 | number: '1.0-0' }}% by upgrading to SegWit-P2SH" placement="bottom"><del>SegWit</del></span>
|
||||
<span *ngIf="isRbfTransaction" class="badge badge-success" ngbTooltip="This transaction support Replace-By-Fee (RBF) allowing fee bumping" placement="bottom">RBF</span>
|
@ -0,0 +1,30 @@
|
||||
import { Component, ChangeDetectionStrategy, OnChanges, Input } from '@angular/core';
|
||||
import { calcSegwitFeeGains } from 'src/app/bitcoin.utils';
|
||||
import { Transaction } from 'src/app/interfaces/electrs.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tx-features',
|
||||
templateUrl: './tx-features.component.html',
|
||||
styleUrls: ['./tx-features.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TxFeaturesComponent implements OnChanges {
|
||||
@Input() tx: Transaction;
|
||||
|
||||
segwitGains = {
|
||||
realizedGains: 0,
|
||||
potentialBech32Gains: 0,
|
||||
potentialP2shGains: 0,
|
||||
};
|
||||
isRbfTransaction: boolean;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnChanges() {
|
||||
if (!this.tx) {
|
||||
return;
|
||||
}
|
||||
this.segwitGains = calcSegwitFeeGains(this.tx);
|
||||
this.isRbfTransaction = this.tx.vin.some((v) => v.sequence < 0xfffffffe);
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
<span *ngIf="feeRating === 1" class="badge badge-success">Optimal</span>
|
||||
<span *ngIf="feeRating === 2" class="badge badge-warning" title="Only ~{{ medianFeeNeeded }} sat/vB was needed to get into this block">Overpaid {{ overpaidTimes }}x</span>
|
||||
<span *ngIf="feeRating === 3" class="badge badge-danger" title="Only ~{{ medianFeeNeeded }} sat/vB was needed to get into this block">Overpaid {{ overpaidTimes }}x</span>
|
@ -0,0 +1,64 @@
|
||||
import { Component, ChangeDetectionStrategy, OnChanges, Input, OnInit, ChangeDetectorRef } from '@angular/core';
|
||||
import { Transaction, Block } from 'src/app/interfaces/electrs.interface';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tx-fee-rating',
|
||||
templateUrl: './tx-fee-rating.component.html',
|
||||
styleUrls: ['./tx-fee-rating.component.scss'],
|
||||
})
|
||||
export class TxFeeRatingComponent implements OnInit, OnChanges {
|
||||
@Input() tx: Transaction;
|
||||
|
||||
medianFeeNeeded: number;
|
||||
overpaidTimes: number;
|
||||
feeRating: number;
|
||||
|
||||
blocks: Block[] = [];
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.stateService.blocks$.subscribe(([block]) => {
|
||||
this.blocks.push(block);
|
||||
if (this.tx.status.confirmed && this.tx.status.block_height === block.height) {
|
||||
this.calculateRatings(block);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.feeRating = undefined;
|
||||
if (!this.tx.status.confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const foundBlock = this.blocks.find((b) => b.height === this.tx.status.block_height);
|
||||
if (foundBlock) {
|
||||
this.calculateRatings(foundBlock);
|
||||
}
|
||||
}
|
||||
|
||||
calculateRatings(block: Block) {
|
||||
const feePervByte = this.tx.fee / (this.tx.weight / 4);
|
||||
this.medianFeeNeeded = Math.round(block.feeRange[Math.round(block.feeRange.length * 0.5)]);
|
||||
|
||||
// Block not filled
|
||||
if (block.weight < 4000000 * 0.95) {
|
||||
this.medianFeeNeeded = 1;
|
||||
}
|
||||
|
||||
this.overpaidTimes = Math.round(feePervByte / this.medianFeeNeeded);
|
||||
|
||||
if (feePervByte <= this.medianFeeNeeded || this.overpaidTimes < 2) {
|
||||
this.feeRating = 1;
|
||||
} else {
|
||||
this.feeRating = 2;
|
||||
if (this.overpaidTimes > 10) {
|
||||
this.feeRating = 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
|
||||
import { OptimizedMempoolStats } from '../interfaces/node-api.interface';
|
||||
import { Observable } from 'rxjs';
|
||||
import { StateService } from './state.service';
|
||||
@ -18,6 +18,9 @@ export class ApiService {
|
||||
) {
|
||||
this.apiBaseUrl = API_BASE_URL.replace('{network}', '');
|
||||
this.stateService.networkChanged$.subscribe((network) => {
|
||||
if (network === 'bisq') {
|
||||
network = '';
|
||||
}
|
||||
this.apiBaseUrl = API_BASE_URL.replace('{network}', network ? '/' + network : '');
|
||||
});
|
||||
}
|
||||
|
@ -18,6 +18,9 @@ export class ElectrsApiService {
|
||||
) {
|
||||
this.apiBaseUrl = API_BASE_URL.replace('{network}', '');
|
||||
this.stateService.networkChanged$.subscribe((network) => {
|
||||
if (network === 'bisq') {
|
||||
network = '';
|
||||
}
|
||||
this.apiBaseUrl = API_BASE_URL.replace('{network}', network ? '/' + network : '');
|
||||
});
|
||||
}
|
||||
|
@ -18,14 +18,9 @@ export class SeoService {
|
||||
|
||||
setTitle(newTitle: string, prependNetwork = false) {
|
||||
let networkName = '';
|
||||
if (prependNetwork) {
|
||||
if (this.network === 'liquid') {
|
||||
networkName = 'Liquid ';
|
||||
} else if (this.network === 'testnet') {
|
||||
networkName = 'Testnet ';
|
||||
}
|
||||
if (prependNetwork && this.network !== '') {
|
||||
networkName = this.network.substr(0, 1).toUpperCase() + this.network.substr(1) + ' ';
|
||||
}
|
||||
|
||||
this.titleService.setTitle(networkName + newTitle + ' - ' + this.defaultTitle);
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,7 @@ export class StateService {
|
||||
networkChanged$ = new ReplaySubject<string>(1);
|
||||
blocks$ = new ReplaySubject<[Block, boolean, boolean]>(env.KEEP_BLOCKS_AMOUNT);
|
||||
conversions$ = new ReplaySubject<any>(1);
|
||||
bsqPrice$ = new ReplaySubject<number>(1);
|
||||
mempoolStats$ = new ReplaySubject<MemPoolState>(1);
|
||||
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
|
||||
txReplaced$ = new Subject<Transaction>();
|
||||
@ -64,6 +65,12 @@ export class StateService {
|
||||
this.networkChanged$.next('testnet');
|
||||
}
|
||||
return;
|
||||
case 'bisq':
|
||||
if (this.network !== 'bisq') {
|
||||
this.network = 'bisq';
|
||||
this.networkChanged$.next('bisq');
|
||||
}
|
||||
return;
|
||||
default:
|
||||
if (this.network !== '') {
|
||||
this.network = '';
|
||||
|
@ -29,11 +29,14 @@ export class WebsocketService {
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
) {
|
||||
this.network = this.stateService.network;
|
||||
this.websocketSubject = webSocket<WebsocketResponse | any>(WEB_SOCKET_URL.replace('{network}', this.network ? '/' + this.network : ''));
|
||||
this.network = this.stateService.network === 'bisq' ? '' : this.stateService.network;
|
||||
this.websocketSubject = webSocket<WebsocketResponse>(WEB_SOCKET_URL.replace('{network}', this.network ? '/' + this.network : ''));
|
||||
this.startSubscription();
|
||||
|
||||
this.stateService.networkChanged$.subscribe((network) => {
|
||||
if (network === 'bisq') {
|
||||
network = '';
|
||||
}
|
||||
if (network === this.network) {
|
||||
return;
|
||||
}
|
||||
@ -45,7 +48,7 @@ export class WebsocketService {
|
||||
|
||||
this.websocketSubject.complete();
|
||||
this.subscription.unsubscribe();
|
||||
this.websocketSubject = webSocket<WebsocketResponse | any>(WEB_SOCKET_URL.replace('{network}', this.network ? '/' + this.network : ''));
|
||||
this.websocketSubject = webSocket<WebsocketResponse>(WEB_SOCKET_URL.replace('{network}', this.network ? '/' + this.network : ''));
|
||||
|
||||
this.startSubscription();
|
||||
});
|
||||
@ -96,6 +99,10 @@ export class WebsocketService {
|
||||
this.stateService.mempoolBlocks$.next(response['mempool-blocks']);
|
||||
}
|
||||
|
||||
if (response['bsq-price']) {
|
||||
this.stateService.bsqPrice$.next(response['bsq-price']);
|
||||
}
|
||||
|
||||
if (response['git-commit']) {
|
||||
if (!this.latestGitCommit) {
|
||||
this.latestGitCommit = response['git-commit'];
|
||||
|
64
frontend/src/app/shared/shared.module.ts
Normal file
64
frontend/src/app/shared/shared.module.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { VbytesPipe } from './pipes/bytes-pipe/vbytes.pipe';
|
||||
import { ShortenStringPipe } from './pipes/shorten-string-pipe/shorten-string.pipe';
|
||||
import { CeilPipe } from './pipes/math-ceil/math-ceil.pipe';
|
||||
import { Hex2asciiPipe } from './pipes/hex2ascii/hex2ascii.pipe';
|
||||
import { RelativeUrlPipe } from './pipes/relative-url/relative-url.pipe';
|
||||
import { ScriptpubkeyTypePipe } from './pipes/scriptpubkey-type-pipe/scriptpubkey-type.pipe';
|
||||
import { BytesPipe } from './pipes/bytes-pipe/bytes.pipe';
|
||||
import { WuBytesPipe } from './pipes/bytes-pipe/wubytes.pipe';
|
||||
import { TimeSinceComponent } from '../components/time-since/time-since.component';
|
||||
import { ClipboardComponent } from '../components/clipboard/clipboard.component';
|
||||
import { QrcodeComponent } from '../components/qrcode/qrcode.component';
|
||||
import { FiatComponent } from '../fiat/fiat.component';
|
||||
import { NgbNavModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TxFeaturesComponent } from '../components/tx-features/tx-features.component';
|
||||
import { TxFeeRatingComponent } from '../components/tx-fee-rating/tx-fee-rating.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ClipboardComponent,
|
||||
TimeSinceComponent,
|
||||
QrcodeComponent,
|
||||
FiatComponent,
|
||||
TxFeaturesComponent,
|
||||
TxFeeRatingComponent,
|
||||
ScriptpubkeyTypePipe,
|
||||
RelativeUrlPipe,
|
||||
Hex2asciiPipe,
|
||||
BytesPipe,
|
||||
VbytesPipe,
|
||||
WuBytesPipe,
|
||||
CeilPipe,
|
||||
ShortenStringPipe,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
NgbNavModule,
|
||||
NgbTooltipModule,
|
||||
],
|
||||
providers: [
|
||||
VbytesPipe,
|
||||
],
|
||||
exports: [
|
||||
NgbNavModule,
|
||||
CommonModule,
|
||||
NgbTooltipModule,
|
||||
TimeSinceComponent,
|
||||
ClipboardComponent,
|
||||
QrcodeComponent,
|
||||
FiatComponent,
|
||||
TxFeaturesComponent,
|
||||
TxFeeRatingComponent,
|
||||
ScriptpubkeyTypePipe,
|
||||
RelativeUrlPipe,
|
||||
Hex2asciiPipe,
|
||||
BytesPipe,
|
||||
VbytesPipe,
|
||||
WuBytesPipe,
|
||||
CeilPipe,
|
||||
ShortenStringPipe,
|
||||
]
|
||||
})
|
||||
export class SharedModule {}
|
BIN
frontend/src/resources/bisq-logo.png
Normal file
BIN
frontend/src/resources/bisq-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 11 KiB |
Binary file not shown.
Before Width: | Height: | Size: 62 KiB |
@ -171,7 +171,7 @@ body {
|
||||
}
|
||||
|
||||
.table-striped tbody tr:nth-of-type(odd) {
|
||||
background-color: #181b2d !important;
|
||||
background-color: #181b2d;
|
||||
}
|
||||
|
||||
.bordertop {
|
||||
@ -375,7 +375,7 @@ h1, h2, h3 {
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.md-inline {
|
||||
.d-md-inline {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
@ -416,3 +416,7 @@ h1, h2, h3 {
|
||||
background-color: #653b9c;
|
||||
border-color: #3a1c61;
|
||||
}
|
||||
|
||||
th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
@ -1189,6 +1189,30 @@
|
||||
lodash "^4.17.13"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@fortawesome/angular-fontawesome@^0.6.1":
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.6.1.tgz#1ebe5db16bfdd4be44bdde61f78c760eb4e219fa"
|
||||
integrity sha512-ARQjtRuT+ZskzJDJKPwuiGO3+7nS0iyNLU/uHVJHfG4LwGJxwVIGldwg1SU957sra0Z0OtWEajHMhiS4vB9LwQ==
|
||||
|
||||
"@fortawesome/fontawesome-common-types@^0.2.29":
|
||||
version "0.2.29"
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.29.tgz#e1a456b643237462d390304cab6975ff3fd68397"
|
||||
integrity sha512-cY+QfDTbZ7XVxzx7jxbC98Oxr/zc7R2QpTLqTxqlfyXDrAJjzi/xUIqAUsygELB62JIrbsWxtSRhayKFkGI7MA==
|
||||
|
||||
"@fortawesome/fontawesome-svg-core@^1.2.28":
|
||||
version "1.2.29"
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.29.tgz#34ef32824664534f9e4ef37982ebf286b899a189"
|
||||
integrity sha512-xmPmP2t8qrdo8RyKihTkGb09RnZoc+7HFBCnr0/6ZhStdGDSLeEd7ajV181+2W29NWIFfylO13rU+s3fpy3cnA==
|
||||
dependencies:
|
||||
"@fortawesome/fontawesome-common-types" "^0.2.29"
|
||||
|
||||
"@fortawesome/free-solid-svg-icons@^5.13.0":
|
||||
version "5.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.13.1.tgz#010a846b718a0f110b3cd137d072639b4e8bd41a"
|
||||
integrity sha512-LQH/0L1p4+rqtoSHa9qFYR84hpuRZKqaQ41cfBQx8b68p21zoWSekTAeA54I/2x9VlCHDLFlG74Nmdg4iTPQOg==
|
||||
dependencies:
|
||||
"@fortawesome/fontawesome-common-types" "^0.2.29"
|
||||
|
||||
"@istanbuljs/schema@^0.1.2":
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd"
|
||||
|
@ -223,5 +223,9 @@ http {
|
||||
location /testnet/api/ {
|
||||
proxy_pass http://[::1]:3002/;
|
||||
}
|
||||
|
||||
location /bisq/api {
|
||||
proxy_pass http://127.0.0.1:8999/api/v1/bisq;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user