Merge branch 'master' into simon/eslint-triple-equals
This commit is contained in:
commit
1aad4f2926
@ -7,11 +7,14 @@ const config: Config.InitialOptions = {
|
||||
automock: false,
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ["./src/**/**.ts"],
|
||||
coverageProvider: "v8",
|
||||
coverageProvider: "babel",
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
setupFiles: [
|
||||
"./testSetup.ts",
|
||||
],
|
||||
}
|
||||
export default config;
|
||||
|
||||
62
backend/src/__tests__/api/difficulty-adjustment.test.ts
Normal file
62
backend/src/__tests__/api/difficulty-adjustment.test.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { calcDifficultyAdjustment, DifficultyAdjustment } from '../../api/difficulty-adjustment';
|
||||
|
||||
describe('Mempool Difficulty Adjustment', () => {
|
||||
test('should calculate Difficulty Adjustments properly', () => {
|
||||
const dt = (dtString) => {
|
||||
return Math.floor(new Date(dtString).getTime() / 1000);
|
||||
};
|
||||
|
||||
const vectors = [
|
||||
[ // Vector 1
|
||||
[ // Inputs
|
||||
dt('2022-08-18T11:07:00.000Z'), // Last DA time (in seconds)
|
||||
dt('2022-08-19T14:03:53.000Z'), // Current time (now) (in seconds)
|
||||
750134, // Current block height
|
||||
0.6280047707459726, // Previous retarget % (Passed through)
|
||||
'mainnet', // Network (if testnet, next value is non-zero)
|
||||
0, // If not testnet, not used
|
||||
],
|
||||
{ // Expected Result
|
||||
progressPercent: 9.027777777777777,
|
||||
difficultyChange: 12.562233927411782,
|
||||
estimatedRetargetDate: 1661895424692,
|
||||
remainingBlocks: 1834,
|
||||
remainingTime: 977591692,
|
||||
previousRetarget: 0.6280047707459726,
|
||||
nextRetargetHeight: 751968,
|
||||
timeAvg: 533038,
|
||||
timeOffset: 0,
|
||||
},
|
||||
],
|
||||
[ // Vector 2 (testnet)
|
||||
[ // Inputs
|
||||
dt('2022-08-18T11:07:00.000Z'), // Last DA time (in seconds)
|
||||
dt('2022-08-19T14:03:53.000Z'), // Current time (now) (in seconds)
|
||||
750134, // Current block height
|
||||
0.6280047707459726, // Previous retarget % (Passed through)
|
||||
'testnet', // Network
|
||||
dt('2022-08-19T13:52:46.000Z'), // Latest block timestamp in seconds
|
||||
],
|
||||
{ // Expected Result is same other than timeOffset
|
||||
progressPercent: 9.027777777777777,
|
||||
difficultyChange: 12.562233927411782,
|
||||
estimatedRetargetDate: 1661895424692,
|
||||
remainingBlocks: 1834,
|
||||
remainingTime: 977591692,
|
||||
previousRetarget: 0.6280047707459726,
|
||||
nextRetargetHeight: 751968,
|
||||
timeAvg: 533038,
|
||||
timeOffset: -667000, // 11 min 7 seconds since last block (testnet only)
|
||||
// If we add time avg to abs(timeOffset) it makes exactly 1200000 ms, or 20 minutes
|
||||
},
|
||||
],
|
||||
] as [[number, number, number, number, string, number], DifficultyAdjustment][];
|
||||
|
||||
for (const vector of vectors) {
|
||||
const result = calcDifficultyAdjustment(...vector[0]);
|
||||
// previousRetarget is passed through untouched
|
||||
expect(result.previousRetarget).toStrictEqual(vector[0][3]);
|
||||
expect(result).toStrictEqual(vector[1]);
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -136,5 +136,4 @@ describe('Mempool Backend Config', () => {
|
||||
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@ -510,7 +510,12 @@ class BitcoinRoutes {
|
||||
|
||||
private getDifficultyChange(req: Request, res: Response) {
|
||||
try {
|
||||
res.json(difficultyAdjustment.getDifficultyAdjustment());
|
||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||
if (da) {
|
||||
res.json(da);
|
||||
} else {
|
||||
res.status(503).send(`Service Temporarily Unavailable`);
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
|
||||
@ -228,34 +228,75 @@ export class Common {
|
||||
return d.toISOString().split('T')[0] + ' ' + d.toTimeString().split(' ')[0];
|
||||
}
|
||||
|
||||
static formatSocket(publicKey: string, socket: {network: string, addr: string}): NodeSocket {
|
||||
static findSocketNetwork(addr: string): {network: string | null, url: string} {
|
||||
let network: string | null = null;
|
||||
let url = addr.split('://')[1];
|
||||
|
||||
if (config.LIGHTNING.BACKEND === 'cln') {
|
||||
network = socket.network;
|
||||
} else if (config.LIGHTNING.BACKEND === 'lnd') {
|
||||
if (socket.addr.indexOf('onion') !== -1) {
|
||||
if (socket.addr.split('.')[0].length >= 56) {
|
||||
network = 'torv3';
|
||||
} else {
|
||||
network = 'torv2';
|
||||
}
|
||||
} else if (socket.addr.indexOf('i2p') !== -1) {
|
||||
network = 'i2p';
|
||||
if (!url) {
|
||||
return {
|
||||
network: null,
|
||||
url: addr,
|
||||
};
|
||||
}
|
||||
|
||||
if (addr.indexOf('onion') !== -1) {
|
||||
if (url.split('.')[0].length >= 56) {
|
||||
network = 'torv3';
|
||||
} else {
|
||||
const ipv = isIP(socket.addr.split(':')[0]);
|
||||
if (ipv === 4) {
|
||||
network = 'ipv4';
|
||||
} else if (ipv === 6) {
|
||||
network = 'ipv6';
|
||||
}
|
||||
network = 'torv2';
|
||||
}
|
||||
} else if (addr.indexOf('i2p') !== -1) {
|
||||
network = 'i2p';
|
||||
} else if (addr.indexOf('ipv4') !== -1) {
|
||||
const ipv = isIP(url.split(':')[0]);
|
||||
if (ipv === 4) {
|
||||
network = 'ipv4';
|
||||
} else {
|
||||
return {
|
||||
network: null,
|
||||
url: addr,
|
||||
};
|
||||
}
|
||||
} else if (addr.indexOf('ipv6') !== -1) {
|
||||
url = url.split('[')[1].split(']')[0];
|
||||
const ipv = isIP(url);
|
||||
if (ipv === 6) {
|
||||
const parts = addr.split(':');
|
||||
network = 'ipv6';
|
||||
url = `[${url}]:${parts[parts.length - 1]}`;
|
||||
} else {
|
||||
return {
|
||||
network: null,
|
||||
url: addr,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
network: null,
|
||||
url: addr,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
publicKey: publicKey,
|
||||
network: network,
|
||||
addr: socket.addr,
|
||||
url: url,
|
||||
};
|
||||
}
|
||||
|
||||
static formatSocket(publicKey: string, socket: {network: string, addr: string}): NodeSocket {
|
||||
if (config.LIGHTNING.BACKEND === 'cln') {
|
||||
return {
|
||||
publicKey: publicKey,
|
||||
network: socket.network,
|
||||
addr: socket.addr,
|
||||
};
|
||||
} else /* if (config.LIGHTNING.BACKEND === 'lnd') */ {
|
||||
const formatted = this.findSocketNetwork(socket.addr);
|
||||
return {
|
||||
publicKey: publicKey,
|
||||
network: formatted.network,
|
||||
addr: formatted.url,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import logger from '../logger';
|
||||
import { Common } from './common';
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 39;
|
||||
private static currentVersion = 40;
|
||||
private queryTimeout = 120000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
@ -342,6 +342,12 @@ class DatabaseMigration {
|
||||
await this.$executeQuery('ALTER TABLE `nodes` ADD alias_search TEXT NULL DEFAULT NULL AFTER `alias`');
|
||||
await this.$executeQuery('ALTER TABLE nodes ADD FULLTEXT(alias_search)');
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 40 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `nodes` ADD capacity bigint(20) unsigned DEFAULT NULL');
|
||||
await this.$executeQuery('ALTER TABLE `nodes` ADD channels int(11) unsigned DEFAULT NULL');
|
||||
await this.$executeQuery('ALTER TABLE `nodes` ADD INDEX `capacity` (`capacity`);');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -2,65 +2,100 @@ import config from '../config';
|
||||
import { IDifficultyAdjustment } from '../mempool.interfaces';
|
||||
import blocks from './blocks';
|
||||
|
||||
class DifficultyAdjustmentApi {
|
||||
constructor() { }
|
||||
export interface DifficultyAdjustment {
|
||||
progressPercent: number; // Percent: 0 to 100
|
||||
difficultyChange: number; // Percent: -75 to 300
|
||||
estimatedRetargetDate: number; // Unix time in ms
|
||||
remainingBlocks: number; // Block count
|
||||
remainingTime: number; // Duration of time in ms
|
||||
previousRetarget: number; // Percent: -75 to 300
|
||||
nextRetargetHeight: number; // Block Height
|
||||
timeAvg: number; // Duration of time in ms
|
||||
timeOffset: number; // (Testnet) Time since last block (cap @ 20min) in ms
|
||||
}
|
||||
|
||||
public getDifficultyAdjustment(): IDifficultyAdjustment {
|
||||
export function calcDifficultyAdjustment(
|
||||
DATime: number,
|
||||
nowSeconds: number,
|
||||
blockHeight: number,
|
||||
previousRetarget: number,
|
||||
network: string,
|
||||
latestBlockTimestamp: number,
|
||||
): DifficultyAdjustment {
|
||||
const ESTIMATE_LAG_BLOCKS = 146; // For first 7.2% of epoch, don't estimate.
|
||||
const EPOCH_BLOCK_LENGTH = 2016; // Bitcoin mainnet
|
||||
const BLOCK_SECONDS_TARGET = 600; // Bitcoin mainnet
|
||||
const TESTNET_MAX_BLOCK_SECONDS = 1200; // Bitcoin testnet
|
||||
|
||||
const diffSeconds = nowSeconds - DATime;
|
||||
const blocksInEpoch = (blockHeight >= 0) ? blockHeight % EPOCH_BLOCK_LENGTH : 0;
|
||||
const progressPercent = (blockHeight >= 0) ? blocksInEpoch / EPOCH_BLOCK_LENGTH * 100 : 100;
|
||||
const remainingBlocks = EPOCH_BLOCK_LENGTH - blocksInEpoch;
|
||||
const nextRetargetHeight = (blockHeight >= 0) ? blockHeight + remainingBlocks : 0;
|
||||
|
||||
let difficultyChange = 0;
|
||||
let timeAvgSecs = BLOCK_SECONDS_TARGET;
|
||||
// Only calculate the estimate once we have 7.2% of blocks in current epoch
|
||||
if (blocksInEpoch >= ESTIMATE_LAG_BLOCKS) {
|
||||
timeAvgSecs = diffSeconds / blocksInEpoch;
|
||||
difficultyChange = (BLOCK_SECONDS_TARGET / timeAvgSecs - 1) * 100;
|
||||
// Max increase is x4 (+300%)
|
||||
if (difficultyChange > 300) {
|
||||
difficultyChange = 300;
|
||||
}
|
||||
// Max decrease is /4 (-75%)
|
||||
if (difficultyChange < -75) {
|
||||
difficultyChange = -75;
|
||||
}
|
||||
}
|
||||
|
||||
// Testnet difficulty is set to 1 after 20 minutes of no blocks,
|
||||
// therefore the time between blocks will always be below 20 minutes (1200s).
|
||||
let timeOffset = 0;
|
||||
if (network === 'testnet') {
|
||||
if (timeAvgSecs > TESTNET_MAX_BLOCK_SECONDS) {
|
||||
timeAvgSecs = TESTNET_MAX_BLOCK_SECONDS;
|
||||
}
|
||||
|
||||
const secondsSinceLastBlock = nowSeconds - latestBlockTimestamp;
|
||||
if (secondsSinceLastBlock + timeAvgSecs > TESTNET_MAX_BLOCK_SECONDS) {
|
||||
timeOffset = -Math.min(secondsSinceLastBlock, TESTNET_MAX_BLOCK_SECONDS) * 1000;
|
||||
}
|
||||
}
|
||||
|
||||
const timeAvg = Math.floor(timeAvgSecs * 1000);
|
||||
const remainingTime = remainingBlocks * timeAvg;
|
||||
const estimatedRetargetDate = remainingTime + nowSeconds * 1000;
|
||||
|
||||
return {
|
||||
progressPercent,
|
||||
difficultyChange,
|
||||
estimatedRetargetDate,
|
||||
remainingBlocks,
|
||||
remainingTime,
|
||||
previousRetarget,
|
||||
nextRetargetHeight,
|
||||
timeAvg,
|
||||
timeOffset,
|
||||
};
|
||||
}
|
||||
|
||||
class DifficultyAdjustmentApi {
|
||||
public getDifficultyAdjustment(): IDifficultyAdjustment | null {
|
||||
const DATime = blocks.getLastDifficultyAdjustmentTime();
|
||||
const previousRetarget = blocks.getPreviousDifficultyRetarget();
|
||||
const blockHeight = blocks.getCurrentBlockHeight();
|
||||
const blocksCache = blocks.getBlocks();
|
||||
const latestBlock = blocksCache[blocksCache.length - 1];
|
||||
|
||||
const now = new Date().getTime() / 1000;
|
||||
const diff = now - DATime;
|
||||
const blocksInEpoch = blockHeight % 2016;
|
||||
const progressPercent = (blocksInEpoch >= 0) ? blocksInEpoch / 2016 * 100 : 100;
|
||||
const remainingBlocks = 2016 - blocksInEpoch;
|
||||
const nextRetargetHeight = blockHeight + remainingBlocks;
|
||||
|
||||
let difficultyChange = 0;
|
||||
if (remainingBlocks < 1870) {
|
||||
if (blocksInEpoch > 0) {
|
||||
difficultyChange = (600 / (diff / blocksInEpoch) - 1) * 100;
|
||||
}
|
||||
if (difficultyChange > 300) {
|
||||
difficultyChange = 300;
|
||||
}
|
||||
if (difficultyChange < -75) {
|
||||
difficultyChange = -75;
|
||||
}
|
||||
if (!latestBlock) {
|
||||
return null;
|
||||
}
|
||||
const nowSeconds = Math.floor(new Date().getTime() / 1000);
|
||||
|
||||
let timeAvgMins = blocksInEpoch && blocksInEpoch > 146 ? diff / blocksInEpoch / 60 : 10;
|
||||
|
||||
// Testnet difficulty is set to 1 after 20 minutes of no blocks,
|
||||
// therefore the time between blocks will always be below 20 minutes (1200s).
|
||||
let timeOffset = 0;
|
||||
if (config.MEMPOOL.NETWORK === 'testnet') {
|
||||
if (timeAvgMins > 20) {
|
||||
timeAvgMins = 20;
|
||||
}
|
||||
if (now - latestBlock.timestamp + timeAvgMins * 60 > 1200) {
|
||||
timeOffset = -Math.min(now - latestBlock.timestamp, 1200) * 1000;
|
||||
}
|
||||
}
|
||||
|
||||
const timeAvg = timeAvgMins * 60 * 1000 ;
|
||||
const remainingTime = (remainingBlocks * timeAvg) + (now * 1000);
|
||||
const estimatedRetargetDate = remainingTime + now;
|
||||
|
||||
return {
|
||||
progressPercent,
|
||||
difficultyChange,
|
||||
estimatedRetargetDate,
|
||||
remainingBlocks,
|
||||
remainingTime,
|
||||
previousRetarget,
|
||||
nextRetargetHeight,
|
||||
timeAvg,
|
||||
timeOffset,
|
||||
};
|
||||
return calcDifficultyAdjustment(
|
||||
DATime, nowSeconds, blockHeight, previousRetarget,
|
||||
config.MEMPOOL.NETWORK, latestBlock.timestamp
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -80,7 +80,7 @@ class ChannelsApi {
|
||||
public async $searchChannelsById(search: string): Promise<any[]> {
|
||||
try {
|
||||
const searchStripped = search.replace('%', '') + '%';
|
||||
const query = `SELECT id, short_id, capacity FROM channels WHERE id LIKE ? OR short_id LIKE ? LIMIT 10`;
|
||||
const query = `SELECT id, short_id, capacity, status FROM channels WHERE id LIKE ? OR short_id LIKE ? LIMIT 10`;
|
||||
const [rows]: any = await DB.query(query, [searchStripped, searchStripped]);
|
||||
return rows;
|
||||
} catch (e) {
|
||||
@ -229,9 +229,14 @@ class ChannelsApi {
|
||||
|
||||
public async $getChannelsByTransactionId(transactionIds: string[]): Promise<any[]> {
|
||||
try {
|
||||
transactionIds = transactionIds.map((id) => '\'' + id + '\'');
|
||||
const query = `SELECT n1.alias AS alias_left, n2.alias AS alias_right, channels.* FROM channels LEFT JOIN nodes AS n1 ON n1.public_key = channels.node1_public_key LEFT JOIN nodes AS n2 ON n2.public_key = channels.node2_public_key WHERE channels.transaction_id IN (${transactionIds.join(', ')}) OR channels.closing_transaction_id IN (${transactionIds.join(', ')})`;
|
||||
const [rows]: any = await DB.query(query);
|
||||
const query = `
|
||||
SELECT n1.alias AS alias_left, n2.alias AS alias_right, channels.*
|
||||
FROM channels
|
||||
LEFT JOIN nodes AS n1 ON n1.public_key = channels.node1_public_key
|
||||
LEFT JOIN nodes AS n2 ON n2.public_key = channels.node2_public_key
|
||||
WHERE channels.transaction_id IN ? OR channels.closing_transaction_id IN ?
|
||||
`;
|
||||
const [rows]: any = await DB.query(query, [[transactionIds], [transactionIds]]);
|
||||
const channels = rows.map((row) => this.convertChannel(row));
|
||||
return channels;
|
||||
} catch (e) {
|
||||
@ -245,15 +250,20 @@ class ChannelsApi {
|
||||
let channelStatusFilter;
|
||||
if (status === 'open') {
|
||||
channelStatusFilter = '< 2';
|
||||
} else if (status === 'active') {
|
||||
channelStatusFilter = '= 1';
|
||||
} else if (status === 'closed') {
|
||||
channelStatusFilter = '= 2';
|
||||
} else {
|
||||
throw new Error('getChannelsForNode: Invalid status requested');
|
||||
}
|
||||
|
||||
// Channels originating from node
|
||||
let query = `
|
||||
SELECT COALESCE(node2.alias, SUBSTRING(node2_public_key, 0, 20)) AS alias, COALESCE(node2.public_key, node2_public_key) AS public_key,
|
||||
channels.status, channels.node1_fee_rate,
|
||||
channels.capacity, channels.short_id, channels.id, channels.closing_reason
|
||||
channels.capacity, channels.short_id, channels.id, channels.closing_reason,
|
||||
UNIX_TIMESTAMP(closing_date) as closing_date, UNIX_TIMESTAMP(channels.updated_at) as updated_at
|
||||
FROM channels
|
||||
LEFT JOIN nodes AS node2 ON node2.public_key = channels.node2_public_key
|
||||
WHERE node1_public_key = ? AND channels.status ${channelStatusFilter}
|
||||
@ -264,7 +274,8 @@ class ChannelsApi {
|
||||
query = `
|
||||
SELECT COALESCE(node1.alias, SUBSTRING(node1_public_key, 0, 20)) AS alias, COALESCE(node1.public_key, node1_public_key) AS public_key,
|
||||
channels.status, channels.node2_fee_rate,
|
||||
channels.capacity, channels.short_id, channels.id, channels.closing_reason
|
||||
channels.capacity, channels.short_id, channels.id, channels.closing_reason,
|
||||
UNIX_TIMESTAMP(closing_date) as closing_date, UNIX_TIMESTAMP(channels.updated_at) as updated_at
|
||||
FROM channels
|
||||
LEFT JOIN nodes AS node1 ON node1.public_key = channels.node1_public_key
|
||||
WHERE node2_public_key = ? AND channels.status ${channelStatusFilter}
|
||||
@ -273,27 +284,56 @@ class ChannelsApi {
|
||||
|
||||
let allChannels = channelsFromNode.concat(channelsToNode);
|
||||
allChannels.sort((a, b) => {
|
||||
return b.capacity - a.capacity;
|
||||
if (status === 'closed') {
|
||||
if (!b.closing_date && !a.closing_date) {
|
||||
return (b.updated_at ?? 0) - (a.updated_at ?? 0);
|
||||
} else {
|
||||
return (b.closing_date ?? 0) - (a.closing_date ?? 0);
|
||||
}
|
||||
} else {
|
||||
return b.capacity - a.capacity;
|
||||
}
|
||||
});
|
||||
allChannels = allChannels.slice(index, index + length);
|
||||
|
||||
if (index >= 0) {
|
||||
allChannels = allChannels.slice(index, index + length);
|
||||
} else if (index === -1) { // Node channels tree chart
|
||||
allChannels = allChannels.slice(0, 1000);
|
||||
}
|
||||
|
||||
const channels: any[] = []
|
||||
for (const row of allChannels) {
|
||||
const activeChannelsStats: any = await nodesApi.$getActiveChannelsStats(row.public_key);
|
||||
channels.push({
|
||||
status: row.status,
|
||||
closing_reason: row.closing_reason,
|
||||
capacity: row.capacity ?? 0,
|
||||
short_id: row.short_id,
|
||||
id: row.id,
|
||||
fee_rate: row.node1_fee_rate ?? row.node2_fee_rate ?? 0,
|
||||
node: {
|
||||
alias: row.alias.length > 0 ? row.alias : row.public_key.slice(0, 20),
|
||||
public_key: row.public_key,
|
||||
channels: activeChannelsStats.active_channel_count ?? 0,
|
||||
capacity: activeChannelsStats.capacity ?? 0,
|
||||
}
|
||||
});
|
||||
let channel;
|
||||
if (index >= 0) {
|
||||
const activeChannelsStats: any = await nodesApi.$getActiveChannelsStats(row.public_key);
|
||||
channel = {
|
||||
status: row.status,
|
||||
closing_reason: row.closing_reason,
|
||||
closing_date: row.closing_date,
|
||||
capacity: row.capacity ?? 0,
|
||||
short_id: row.short_id,
|
||||
id: row.id,
|
||||
fee_rate: row.node1_fee_rate ?? row.node2_fee_rate ?? 0,
|
||||
node: {
|
||||
alias: row.alias.length > 0 ? row.alias : row.public_key.slice(0, 20),
|
||||
public_key: row.public_key,
|
||||
channels: activeChannelsStats.active_channel_count ?? 0,
|
||||
capacity: activeChannelsStats.capacity ?? 0,
|
||||
}
|
||||
};
|
||||
} else if (index === -1) {
|
||||
channel = {
|
||||
capacity: row.capacity ?? 0,
|
||||
short_id: row.short_id,
|
||||
id: row.id,
|
||||
node: {
|
||||
alias: row.alias.length > 0 ? row.alias : row.public_key.slice(0, 20),
|
||||
public_key: row.public_key,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
channels.push(channel);
|
||||
}
|
||||
|
||||
return channels;
|
||||
@ -498,6 +538,23 @@ class ChannelsApi {
|
||||
logger.err('$setChannelsInactive() error: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
public async $getLatestChannelUpdateForNode(publicKey: string): Promise<number> {
|
||||
try {
|
||||
const query = `
|
||||
SELECT MAX(UNIX_TIMESTAMP(updated_at)) as updated_at
|
||||
FROM channels
|
||||
WHERE node1_public_key = ?
|
||||
`;
|
||||
const [rows]: any[] = await DB.query(query, [publicKey]);
|
||||
if (rows.length > 0) {
|
||||
return rows[0].updated_at;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err(`Can't getLatestChannelUpdateForNode for ${publicKey}. Reason ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export default new ChannelsApi();
|
||||
|
||||
@ -47,8 +47,17 @@ class ChannelsRoutes {
|
||||
res.status(400).send('Missing parameter: public_key');
|
||||
return;
|
||||
}
|
||||
|
||||
const index = parseInt(typeof req.query.index === 'string' ? req.query.index : '0', 10) || 0;
|
||||
const status: string = typeof req.query.status === 'string' ? req.query.status : '';
|
||||
|
||||
if (index < -1) {
|
||||
res.status(400).send('Invalid index');
|
||||
}
|
||||
if (['open', 'active', 'closed'].includes(status) === false) {
|
||||
res.status(400).send('Invalid status');
|
||||
}
|
||||
|
||||
const channels = await channelsApi.$getChannelsForNode(req.query.public_key, index, 10, status);
|
||||
const channelsCount = await channelsApi.$getChannelsCountForNode(req.query.public_key, status);
|
||||
res.header('Pragma', 'public');
|
||||
@ -61,7 +70,7 @@ class ChannelsRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async $getChannelsByTransactionIds(req: Request, res: Response) {
|
||||
private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
if (!Array.isArray(req.query.txId)) {
|
||||
res.status(400).send('Not an array');
|
||||
@ -74,27 +83,26 @@ class ChannelsRoutes {
|
||||
}
|
||||
}
|
||||
const channels = await channelsApi.$getChannelsByTransactionId(txIds);
|
||||
const inputs: any[] = [];
|
||||
const outputs: any[] = [];
|
||||
const result: any[] = [];
|
||||
for (const txid of txIds) {
|
||||
const foundChannelInputs = channels.find((channel) => channel.closing_transaction_id === txid);
|
||||
if (foundChannelInputs) {
|
||||
inputs.push(foundChannelInputs);
|
||||
} else {
|
||||
inputs.push(null);
|
||||
const inputs: any = {};
|
||||
const outputs: any = {};
|
||||
// Assuming that we only have one lightning close input in each transaction. This may not be true in the future
|
||||
const foundChannelsFromInput = channels.find((channel) => channel.closing_transaction_id === txid);
|
||||
if (foundChannelsFromInput) {
|
||||
inputs[0] = foundChannelsFromInput;
|
||||
}
|
||||
const foundChannelOutputs = channels.find((channel) => channel.transaction_id === txid);
|
||||
if (foundChannelOutputs) {
|
||||
outputs.push(foundChannelOutputs);
|
||||
} else {
|
||||
outputs.push(null);
|
||||
const foundChannelsFromOutputs = channels.filter((channel) => channel.transaction_id === txid);
|
||||
for (const output of foundChannelsFromOutputs) {
|
||||
outputs[output.transaction_vout] = output;
|
||||
}
|
||||
result.push({
|
||||
inputs,
|
||||
outputs,
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
inputs: inputs,
|
||||
outputs: outputs,
|
||||
});
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
|
||||
@ -115,17 +115,13 @@ class NodesApi {
|
||||
|
||||
public async $getTopCapacityNodes(full: boolean): Promise<ITopNodesPerCapacity[]> {
|
||||
try {
|
||||
let [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(MAX(added)) as maxAdded FROM node_stats');
|
||||
const latestDate = rows[0].maxAdded;
|
||||
|
||||
let rows: any;
|
||||
let query: string;
|
||||
if (full === false) {
|
||||
query = `
|
||||
SELECT nodes.public_key AS publicKey, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias,
|
||||
node_stats.capacity
|
||||
FROM node_stats
|
||||
JOIN nodes ON nodes.public_key = node_stats.public_key
|
||||
WHERE added = FROM_UNIXTIME(${latestDate})
|
||||
nodes.capacity
|
||||
FROM nodes
|
||||
ORDER BY capacity DESC
|
||||
LIMIT 100
|
||||
`;
|
||||
@ -133,16 +129,14 @@ class NodesApi {
|
||||
[rows] = await DB.query(query);
|
||||
} else {
|
||||
query = `
|
||||
SELECT node_stats.public_key AS publicKey, IF(nodes.alias = '', SUBSTRING(node_stats.public_key, 1, 20), alias) as alias,
|
||||
CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity,
|
||||
CAST(COALESCE(node_stats.channels, 0) as INT) as channels,
|
||||
SELECT nodes.public_key AS publicKey, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias,
|
||||
CAST(COALESCE(nodes.capacity, 0) as INT) as capacity,
|
||||
CAST(COALESCE(nodes.channels, 0) as INT) as channels,
|
||||
UNIX_TIMESTAMP(nodes.first_seen) as firstSeen, UNIX_TIMESTAMP(nodes.updated_at) as updatedAt,
|
||||
geo_names_city.names as city, geo_names_country.names as country
|
||||
FROM node_stats
|
||||
RIGHT JOIN nodes ON nodes.public_key = node_stats.public_key
|
||||
FROM nodes
|
||||
LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
|
||||
LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
|
||||
WHERE added = FROM_UNIXTIME(${latestDate})
|
||||
ORDER BY capacity DESC
|
||||
LIMIT 100
|
||||
`;
|
||||
@ -163,17 +157,13 @@ class NodesApi {
|
||||
|
||||
public async $getTopChannelsNodes(full: boolean): Promise<ITopNodesPerChannels[]> {
|
||||
try {
|
||||
let [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(MAX(added)) as maxAdded FROM node_stats');
|
||||
const latestDate = rows[0].maxAdded;
|
||||
|
||||
let rows: any;
|
||||
let query: string;
|
||||
if (full === false) {
|
||||
query = `
|
||||
SELECT nodes.public_key as publicKey, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias,
|
||||
node_stats.channels
|
||||
FROM node_stats
|
||||
JOIN nodes ON nodes.public_key = node_stats.public_key
|
||||
WHERE added = FROM_UNIXTIME(${latestDate})
|
||||
nodes.channels
|
||||
FROM nodes
|
||||
ORDER BY channels DESC
|
||||
LIMIT 100;
|
||||
`;
|
||||
@ -181,16 +171,14 @@ class NodesApi {
|
||||
[rows] = await DB.query(query);
|
||||
} else {
|
||||
query = `
|
||||
SELECT node_stats.public_key AS publicKey, IF(nodes.alias = '', SUBSTRING(node_stats.public_key, 1, 20), alias) as alias,
|
||||
CAST(COALESCE(node_stats.channels, 0) as INT) as channels,
|
||||
CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity,
|
||||
SELECT nodes.public_key AS publicKey, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias,
|
||||
CAST(COALESCE(nodes.channels, 0) as INT) as channels,
|
||||
CAST(COALESCE(nodes.capacity, 0) as INT) as capacity,
|
||||
UNIX_TIMESTAMP(nodes.first_seen) as firstSeen, UNIX_TIMESTAMP(nodes.updated_at) as updatedAt,
|
||||
geo_names_city.names as city, geo_names_country.names as country
|
||||
FROM node_stats
|
||||
RIGHT JOIN nodes ON nodes.public_key = node_stats.public_key
|
||||
FROM nodes
|
||||
LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
|
||||
LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
|
||||
WHERE added = FROM_UNIXTIME(${latestDate})
|
||||
ORDER BY channels DESC
|
||||
LIMIT 100
|
||||
`;
|
||||
@ -260,8 +248,8 @@ class NodesApi {
|
||||
public async $searchNodeByPublicKeyOrAlias(search: string) {
|
||||
try {
|
||||
const publicKeySearch = search.replace('%', '') + '%';
|
||||
const aliasSearch = search.replace(/[-_.]/g, ' ').replace(/[^a-zA-Z ]/g, '').split(' ').map((search) => '+' + search + '*').join(' ');
|
||||
const query = `SELECT nodes.public_key, nodes.alias, node_stats.capacity FROM nodes LEFT JOIN node_stats ON node_stats.public_key = nodes.public_key WHERE nodes.public_key LIKE ? OR MATCH nodes.alias_search AGAINST (? IN BOOLEAN MODE) GROUP BY nodes.public_key ORDER BY node_stats.capacity DESC LIMIT 10`;
|
||||
const aliasSearch = search.replace(/[-_.]/g, ' ').replace(/[^a-zA-Z0-9 ]/g, '').split(' ').map((search) => '+' + search + '*').join(' ');
|
||||
const query = `SELECT public_key, alias, capacity, channels, status FROM nodes WHERE public_key LIKE ? OR MATCH alias_search AGAINST (? IN BOOLEAN MODE) ORDER BY capacity DESC LIMIT 10`;
|
||||
const [rows]: any = await DB.query(query, [publicKeySearch, aliasSearch]);
|
||||
return rows;
|
||||
} catch (e) {
|
||||
@ -276,7 +264,7 @@ class NodesApi {
|
||||
|
||||
// List all channels and the two linked ISP
|
||||
query = `
|
||||
SELECT short_id, capacity,
|
||||
SELECT short_id, channels.capacity,
|
||||
channels.node1_public_key AS node1PublicKey, isp1.names AS isp1, isp1.id as isp1ID,
|
||||
channels.node2_public_key AS node2PublicKey, isp2.names AS isp2, isp2.id as isp2ID
|
||||
FROM channels
|
||||
@ -391,17 +379,11 @@ class NodesApi {
|
||||
public async $getNodesPerCountry(countryId: string) {
|
||||
try {
|
||||
const query = `
|
||||
SELECT nodes.public_key, CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, CAST(COALESCE(node_stats.channels, 0) as INT) as channels,
|
||||
SELECT nodes.public_key, CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, CAST(COALESCE(nodes.channels, 0) as INT) as channels,
|
||||
nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
|
||||
geo_names_city.names as city, geo_names_country.names as country,
|
||||
geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision
|
||||
FROM node_stats
|
||||
JOIN (
|
||||
SELECT public_key, MAX(added) as last_added
|
||||
FROM node_stats
|
||||
GROUP BY public_key
|
||||
) as b ON b.public_key = node_stats.public_key AND b.last_added = node_stats.added
|
||||
RIGHT JOIN nodes ON nodes.public_key = node_stats.public_key
|
||||
FROM nodes
|
||||
LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
|
||||
LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
|
||||
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
|
||||
@ -426,17 +408,11 @@ class NodesApi {
|
||||
public async $getNodesPerISP(ISPId: string) {
|
||||
try {
|
||||
const query = `
|
||||
SELECT nodes.public_key, CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, CAST(COALESCE(node_stats.channels, 0) as INT) as channels,
|
||||
SELECT nodes.public_key, CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, CAST(COALESCE(nodes.channels, 0) as INT) as channels,
|
||||
nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
|
||||
geo_names_city.names as city, geo_names_country.names as country,
|
||||
geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision
|
||||
FROM node_stats
|
||||
JOIN (
|
||||
SELECT public_key, MAX(added) as last_added
|
||||
FROM node_stats
|
||||
GROUP BY public_key
|
||||
) as b ON b.public_key = node_stats.public_key AND b.last_added = node_stats.added
|
||||
RIGHT JOIN nodes ON nodes.public_key = node_stats.public_key
|
||||
FROM nodes
|
||||
LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
|
||||
LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
|
||||
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
|
||||
@ -464,7 +440,6 @@ class NodesApi {
|
||||
FROM nodes
|
||||
JOIN geo_names ON geo_names.id = nodes.country_id AND geo_names.type = 'country'
|
||||
JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
|
||||
JOIN channels ON channels.node1_public_key = nodes.public_key OR channels.node2_public_key = nodes.public_key
|
||||
GROUP BY country_id
|
||||
ORDER BY COUNT(DISTINCT nodes.public_key) DESC
|
||||
`;
|
||||
@ -528,6 +503,18 @@ class NodesApi {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update node sockets
|
||||
*/
|
||||
public async $updateNodeSockets(publicKey: string, sockets: {network: string; addr: string}[]): Promise<void> {
|
||||
const formattedSockets = (sockets.map(a => a.addr).join(',')) ?? '';
|
||||
try {
|
||||
await DB.query(`UPDATE nodes SET sockets = ? WHERE public_key = ?`, [formattedSockets, publicKey]);
|
||||
} catch (e) {
|
||||
logger.err(`Cannot update node sockets for ${publicKey}. Reason: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all nodes not in `nodesPubkeys` as inactive (status = 0)
|
||||
*/
|
||||
@ -555,7 +542,7 @@ class NodesApi {
|
||||
}
|
||||
|
||||
private aliasToSearchText(str: string): string {
|
||||
return str.replace(/[-_.]/g, ' ').replace(/[^a-zA-Z ]/g, '');
|
||||
return str.replace(/[-_.]/g, ' ').replace(/[^a-zA-Z0-9 ]/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@ class StatisticsApi {
|
||||
public async $getLatestStatistics(): Promise<any> {
|
||||
try {
|
||||
const [rows]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY added DESC LIMIT 1`);
|
||||
const [rows2]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY added DESC LIMIT 1 OFFSET 7`);
|
||||
const [rows2]: any = await DB.query(`SELECT * FROM lightning_stats WHERE DATE(added) = DATE(NOW() - INTERVAL 7 DAY)`);
|
||||
return {
|
||||
latest: rows[0],
|
||||
previous: rows2[0],
|
||||
|
||||
@ -13,9 +13,13 @@ export function convertNode(clNode: any): ILightningApi.Node {
|
||||
features: [], // TODO parse and return clNode.feature
|
||||
pub_key: clNode.nodeid,
|
||||
addresses: clNode.addresses?.map((addr) => {
|
||||
let address = addr.address;
|
||||
if (addr.type === 'ipv6') {
|
||||
address = `[${address}]`;
|
||||
}
|
||||
return {
|
||||
network: addr.type,
|
||||
addr: `${addr.address}:${addr.port}`
|
||||
addr: `${address}:${addr.port}`
|
||||
};
|
||||
}) ?? [],
|
||||
last_update: clNode?.last_timestamp ?? 0,
|
||||
|
||||
@ -74,7 +74,7 @@ class Logger {
|
||||
|
||||
private getNetwork(): string {
|
||||
if (config.LIGHTNING.ENABLED) {
|
||||
return 'lightning';
|
||||
return config.MEMPOOL.NETWORK === 'mainnet' ? 'lightning' : `${config.MEMPOOL.NETWORK}-lightning`;
|
||||
}
|
||||
if (config.BISQ.ENABLED) {
|
||||
return 'bisq';
|
||||
|
||||
@ -63,6 +63,9 @@ class NetworkSyncService {
|
||||
let deletedSockets = 0;
|
||||
const graphNodesPubkeys: string[] = [];
|
||||
for (const node of nodes) {
|
||||
const latestUpdated = await channelsApi.$getLatestChannelUpdateForNode(node.pub_key);
|
||||
node.last_update = Math.max(node.last_update, latestUpdated);
|
||||
|
||||
await nodesApi.$saveNode(node);
|
||||
graphNodesPubkeys.push(node.pub_key);
|
||||
++progress;
|
||||
@ -83,7 +86,7 @@ class NetworkSyncService {
|
||||
logger.info(`${progress} nodes updated. ${deletedSockets} sockets deleted`);
|
||||
|
||||
// If a channel if not present in the graph, mark it as inactive
|
||||
nodesApi.$setNodesInactive(graphNodesPubkeys);
|
||||
await nodesApi.$setNodesInactive(graphNodesPubkeys);
|
||||
|
||||
if (config.MAXMIND.ENABLED) {
|
||||
$lookupNodeLocation();
|
||||
@ -98,7 +101,7 @@ class NetworkSyncService {
|
||||
const [closedChannelsRaw]: any[] = await DB.query(`SELECT id FROM channels WHERE status = 2`);
|
||||
const closedChannels = {};
|
||||
for (const closedChannel of closedChannelsRaw) {
|
||||
closedChannels[Common.channelShortIdToIntegerId(closedChannel.id)] = true;
|
||||
closedChannels[closedChannel.id] = true;
|
||||
}
|
||||
|
||||
let progress = 0;
|
||||
@ -121,7 +124,7 @@ class NetworkSyncService {
|
||||
logger.info(`${progress} channels updated`);
|
||||
|
||||
// If a channel if not present in the graph, mark it as inactive
|
||||
channelsApi.$setChannelsInactive(graphChannelsIds);
|
||||
await channelsApi.$setChannelsInactive(graphChannelsIds);
|
||||
} catch (e) {
|
||||
logger.err(`Cannot update channel list. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||
}
|
||||
@ -285,44 +288,66 @@ class NetworkSyncService {
|
||||
for (const channel of channels) {
|
||||
let reason = 0;
|
||||
// Only Esplora backend can retrieve spent transaction outputs
|
||||
const outspends = await bitcoinApi.$getOutspends(channel.closing_transaction_id);
|
||||
const lightningScriptReasons: number[] = [];
|
||||
for (const outspend of outspends) {
|
||||
if (outspend.spent && outspend.txid) {
|
||||
const spendingTx = await bitcoinApi.$getRawTransaction(outspend.txid);
|
||||
const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
|
||||
lightningScriptReasons.push(lightningScript);
|
||||
try {
|
||||
let outspends: IEsploraApi.Outspend[] | undefined;
|
||||
try {
|
||||
outspends = await bitcoinApi.$getOutspends(channel.closing_transaction_id);
|
||||
} catch (e) {
|
||||
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + channel.closing_transaction_id + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (lightningScriptReasons.length === outspends.length
|
||||
&& lightningScriptReasons.filter((r) => r === 1).length === outspends.length) {
|
||||
reason = 1;
|
||||
} else {
|
||||
const filteredReasons = lightningScriptReasons.filter((r) => r !== 1);
|
||||
if (filteredReasons.length) {
|
||||
if (filteredReasons.some((r) => r === 2 || r === 4)) {
|
||||
reason = 3;
|
||||
} else {
|
||||
reason = 2;
|
||||
const lightningScriptReasons: number[] = [];
|
||||
for (const outspend of outspends) {
|
||||
if (outspend.spent && outspend.txid) {
|
||||
let spendingTx: IEsploraApi.Transaction | undefined;
|
||||
try {
|
||||
spendingTx = await bitcoinApi.$getRawTransaction(outspend.txid);
|
||||
} catch (e) {
|
||||
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + outspend.txid}. Reason ${e instanceof Error ? e.message : e}`);
|
||||
continue;
|
||||
}
|
||||
const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
|
||||
lightningScriptReasons.push(lightningScript);
|
||||
}
|
||||
}
|
||||
if (lightningScriptReasons.length === outspends.length
|
||||
&& lightningScriptReasons.filter((r) => r === 1).length === outspends.length) {
|
||||
reason = 1;
|
||||
} else {
|
||||
/*
|
||||
We can detect a commitment transaction (force close) by reading Sequence and Locktime
|
||||
https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction
|
||||
*/
|
||||
const closingTx = await bitcoinApi.$getRawTransaction(channel.closing_transaction_id);
|
||||
const sequenceHex: string = closingTx.vin[0].sequence.toString(16);
|
||||
const locktimeHex: string = closingTx.locktime.toString(16);
|
||||
if (sequenceHex.substring(0, 2) === '80' && locktimeHex.substring(0, 2) === '20') {
|
||||
reason = 2; // Here we can't be sure if it's a penalty or not
|
||||
const filteredReasons = lightningScriptReasons.filter((r) => r !== 1);
|
||||
if (filteredReasons.length) {
|
||||
if (filteredReasons.some((r) => r === 2 || r === 4)) {
|
||||
reason = 3;
|
||||
} else {
|
||||
reason = 2;
|
||||
}
|
||||
} else {
|
||||
reason = 1;
|
||||
/*
|
||||
We can detect a commitment transaction (force close) by reading Sequence and Locktime
|
||||
https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction
|
||||
*/
|
||||
let closingTx: IEsploraApi.Transaction | undefined;
|
||||
try {
|
||||
closingTx = await bitcoinApi.$getRawTransaction(channel.closing_transaction_id);
|
||||
} catch (e) {
|
||||
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + channel.closing_transaction_id}. Reason ${e instanceof Error ? e.message : e}`);
|
||||
continue;
|
||||
}
|
||||
const sequenceHex: string = closingTx.vin[0].sequence.toString(16);
|
||||
const locktimeHex: string = closingTx.locktime.toString(16);
|
||||
if (sequenceHex.substring(0, 2) === '80' && locktimeHex.substring(0, 2) === '20') {
|
||||
reason = 2; // Here we can't be sure if it's a penalty or not
|
||||
} else {
|
||||
reason = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reason) {
|
||||
logger.debug('Setting closing reason ' + reason + ' for channel: ' + channel.id + '.');
|
||||
await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
|
||||
if (reason) {
|
||||
logger.debug('Setting closing reason ' + reason + ' for channel: ' + channel.id + '.');
|
||||
await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err(`$runClosedChannelsForensics() failed for channel ${channel.short_id}. Reason: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
|
||||
++progress;
|
||||
|
||||
@ -4,6 +4,7 @@ import nodesApi from '../../../api/explorer/nodes.api';
|
||||
import config from '../../../config';
|
||||
import DB from '../../../database';
|
||||
import logger from '../../../logger';
|
||||
import * as IPCheck from '../../../utils/ipcheck.js';
|
||||
|
||||
export async function $lookupNodeLocation(): Promise<void> {
|
||||
let loggerTimer = new Date().getTime() / 1000;
|
||||
@ -27,6 +28,26 @@ export async function $lookupNodeLocation(): Promise<void> {
|
||||
const asn = lookupAsn.get(ip);
|
||||
const isp = lookupIsp.get(ip);
|
||||
|
||||
let asOverwrite: any | undefined;
|
||||
if (asn && (IPCheck.match(ip, '170.75.160.0/20') || IPCheck.match(ip, '172.81.176.0/21'))) {
|
||||
asOverwrite = {
|
||||
asn: 394745,
|
||||
name: 'Lunanode',
|
||||
};
|
||||
}
|
||||
else if (asn && (IPCheck.match(ip, '50.7.0.0/16') || IPCheck.match(ip, '66.90.64.0/18'))) {
|
||||
asOverwrite = {
|
||||
asn: 30058,
|
||||
name: 'FDCservers.net',
|
||||
};
|
||||
}
|
||||
else if (asn && asn.autonomous_system_number === 174) {
|
||||
asOverwrite = {
|
||||
asn: 174,
|
||||
name: 'Cogent Communications',
|
||||
};
|
||||
}
|
||||
|
||||
if (city && (asn || isp)) {
|
||||
const query = `
|
||||
UPDATE nodes SET
|
||||
@ -41,7 +62,7 @@ export async function $lookupNodeLocation(): Promise<void> {
|
||||
`;
|
||||
|
||||
const params = [
|
||||
isp?.autonomous_system_number ?? asn?.autonomous_system_number,
|
||||
asOverwrite?.asn ?? isp?.autonomous_system_number ?? asn?.autonomous_system_number,
|
||||
city.city?.geoname_id,
|
||||
city.country?.geoname_id,
|
||||
city.subdivisions ? city.subdivisions[0].geoname_id : null,
|
||||
@ -91,7 +112,10 @@ export async function $lookupNodeLocation(): Promise<void> {
|
||||
if (isp?.autonomous_system_organization ?? asn?.autonomous_system_organization) {
|
||||
await DB.query(
|
||||
`INSERT IGNORE INTO geo_names (id, type, names) VALUES (?, 'as_organization', ?)`,
|
||||
[isp?.autonomous_system_number ?? asn?.autonomous_system_number, JSON.stringify(isp?.isp ?? asn?.autonomous_system_organization)]);
|
||||
[
|
||||
asOverwrite?.asn ?? isp?.autonomous_system_number ?? asn?.autonomous_system_number,
|
||||
JSON.stringify(asOverwrite?.name ?? isp?.isp ?? asn?.autonomous_system_organization)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,8 @@ import { ILightningApi } from '../../../api/lightning/lightning-api.interface';
|
||||
import { isIP } from 'net';
|
||||
import { Common } from '../../../api/common';
|
||||
import channelsApi from '../../../api/explorer/channels.api';
|
||||
import nodesApi from '../../../api/explorer/nodes.api';
|
||||
import { ResultSetHeader } from 'mysql2';
|
||||
|
||||
const fsPromises = promises;
|
||||
|
||||
@ -18,7 +20,12 @@ class LightningStatsImporter {
|
||||
logger.info('Caching funding txs for currently existing channels');
|
||||
await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id));
|
||||
|
||||
if (config.MEMPOOL.NETWORK !== 'mainnet' || config.DATABASE.ENABLED === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.$importHistoricalLightningStats();
|
||||
await this.$cleanupIncorrectSnapshot();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -32,7 +39,28 @@ class LightningStatsImporter {
|
||||
let clearnetTorNodes = 0;
|
||||
let unannouncedNodes = 0;
|
||||
|
||||
const [nodesInDbRaw]: any[] = await DB.query(`SELECT public_key FROM nodes`);
|
||||
const nodesInDb = {};
|
||||
for (const node of nodesInDbRaw) {
|
||||
nodesInDb[node.public_key] = node;
|
||||
}
|
||||
|
||||
for (const node of networkGraph.nodes) {
|
||||
// If we don't know about this node, insert it in db
|
||||
if (isHistorical === true && !nodesInDb[node.pub_key]) {
|
||||
await nodesApi.$saveNode({
|
||||
last_update: node.last_update,
|
||||
pub_key: node.pub_key,
|
||||
alias: node.alias,
|
||||
addresses: node.addresses,
|
||||
color: node.color,
|
||||
features: node.features,
|
||||
});
|
||||
nodesInDb[node.pub_key] = node;
|
||||
} else {
|
||||
await nodesApi.$updateNodeSockets(node.pub_key, node.addresses);
|
||||
}
|
||||
|
||||
let hasOnion = false;
|
||||
let hasClearnet = false;
|
||||
let isUnnanounced = true;
|
||||
@ -69,7 +97,7 @@ class LightningStatsImporter {
|
||||
const baseFees: number[] = [];
|
||||
const alreadyCountedChannels = {};
|
||||
|
||||
const [channelsInDbRaw]: any[] = await DB.query(`SELECT short_id, created FROM channels`);
|
||||
const [channelsInDbRaw]: any[] = await DB.query(`SELECT short_id FROM channels`);
|
||||
const channelsInDb = {};
|
||||
for (const channel of channelsInDbRaw) {
|
||||
channelsInDb[channel.short_id] = channel;
|
||||
@ -84,29 +112,19 @@ class LightningStatsImporter {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Channel is already in db, check if we need to update 'created' field
|
||||
if (isHistorical === true) {
|
||||
//@ts-ignore
|
||||
if (channelsInDb[short_id] && channel.timestamp < channel.created) {
|
||||
await DB.query(`
|
||||
UPDATE channels SET created = FROM_UNIXTIME(?) WHERE channels.short_id = ?`,
|
||||
//@ts-ignore
|
||||
[channel.timestamp, short_id]
|
||||
);
|
||||
} else if (!channelsInDb[short_id]) {
|
||||
await channelsApi.$saveChannel({
|
||||
channel_id: short_id,
|
||||
chan_point: `${tx.txid}:${short_id.split('x')[2]}`,
|
||||
//@ts-ignore
|
||||
last_update: channel.timestamp,
|
||||
node1_pub: channel.node1_pub,
|
||||
node2_pub: channel.node2_pub,
|
||||
capacity: (tx.value * 100000000).toString(),
|
||||
node1_policy: null,
|
||||
node2_policy: null,
|
||||
}, 0);
|
||||
channelsInDb[channel.channel_id] = channel;
|
||||
}
|
||||
// If we don't know about this channel, insert it in db
|
||||
if (isHistorical === true && !channelsInDb[short_id]) {
|
||||
await channelsApi.$saveChannel({
|
||||
channel_id: short_id,
|
||||
chan_point: `${tx.txid}:${short_id.split('x')[2]}`,
|
||||
last_update: channel.last_update,
|
||||
node1_pub: channel.node1_pub,
|
||||
node2_pub: channel.node2_pub,
|
||||
capacity: (tx.value * 100000000).toString(),
|
||||
node1_policy: null,
|
||||
node2_policy: null,
|
||||
}, 0);
|
||||
channelsInDb[channel.channel_id] = channel;
|
||||
}
|
||||
|
||||
if (!nodeStats[channel.node1_pub]) {
|
||||
@ -269,6 +287,17 @@ class LightningStatsImporter {
|
||||
nodeStats[public_key].capacity,
|
||||
nodeStats[public_key].channels,
|
||||
]);
|
||||
|
||||
if (!isHistorical) {
|
||||
await DB.query(
|
||||
`UPDATE nodes SET capacity = ?, channels = ? WHERE public_key = ?`,
|
||||
[
|
||||
nodeStats[public_key].capacity,
|
||||
nodeStats[public_key].channels,
|
||||
public_key,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@ -281,6 +310,7 @@ class LightningStatsImporter {
|
||||
* Import topology files LN historical data into the database
|
||||
*/
|
||||
async $importHistoricalLightningStats(): Promise<void> {
|
||||
logger.debug('Run the historical importer');
|
||||
try {
|
||||
let fileList: string[] = [];
|
||||
try {
|
||||
@ -294,7 +324,7 @@ class LightningStatsImporter {
|
||||
fileList.sort().reverse();
|
||||
|
||||
const [rows]: any[] = await DB.query(`
|
||||
SELECT UNIX_TIMESTAMP(added) AS added, node_count
|
||||
SELECT UNIX_TIMESTAMP(added) AS added
|
||||
FROM lightning_stats
|
||||
ORDER BY added DESC
|
||||
`);
|
||||
@ -341,10 +371,16 @@ class LightningStatsImporter {
|
||||
graph = JSON.parse(fileContent);
|
||||
graph = await this.cleanupTopology(graph);
|
||||
} catch (e) {
|
||||
logger.debug(`Invalid topology file ${this.topologiesFolder}/${filename}, cannot parse the content`);
|
||||
logger.debug(`Invalid topology file ${this.topologiesFolder}/${filename}, cannot parse the content. Reason: ${e instanceof Error ? e.message : e}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.isIncorrectSnapshot(timestamp, graph)) {
|
||||
logger.debug(`Ignoring ${this.topologiesFolder}/${filename}, because we defined it as an incorrect snapshot`);
|
||||
++totalProcessed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!logStarted) {
|
||||
logger.info(`Founds a topology file that we did not import. Importing historical lightning stats now.`);
|
||||
logStarted = true;
|
||||
@ -375,7 +411,7 @@ class LightningStatsImporter {
|
||||
}
|
||||
}
|
||||
|
||||
async cleanupTopology(graph) {
|
||||
cleanupTopology(graph): ILightningApi.NetworkGraph {
|
||||
const newGraph = {
|
||||
nodes: <ILightningApi.Node[]>[],
|
||||
edges: <ILightningApi.Channel[]>[],
|
||||
@ -385,18 +421,23 @@ class LightningStatsImporter {
|
||||
const addressesParts = (node.addresses ?? '').split(',');
|
||||
const addresses: any[] = [];
|
||||
for (const address of addressesParts) {
|
||||
const formatted = Common.findSocketNetwork(address);
|
||||
addresses.push({
|
||||
network: '',
|
||||
addr: address
|
||||
network: formatted.network,
|
||||
addr: formatted.url
|
||||
});
|
||||
}
|
||||
|
||||
let rgb = node.rgb_color ?? '#000000';
|
||||
if (rgb.indexOf('#') === -1) {
|
||||
rgb = `#${rgb}`;
|
||||
}
|
||||
newGraph.nodes.push({
|
||||
last_update: node.timestamp ?? 0,
|
||||
pub_key: node.id ?? null,
|
||||
alias: node.alias ?? null,
|
||||
alias: node.alias ?? node.id.slice(0, 20),
|
||||
addresses: addresses,
|
||||
color: node.rgb_color ?? null,
|
||||
color: rgb,
|
||||
features: {},
|
||||
});
|
||||
}
|
||||
@ -430,6 +471,69 @@ class LightningStatsImporter {
|
||||
|
||||
return newGraph;
|
||||
}
|
||||
|
||||
private isIncorrectSnapshot(timestamp, graph): boolean {
|
||||
if (timestamp >= 1549065600 /* 2019-02-02 */ && timestamp <= 1550620800 /* 2019-02-20 */ && graph.nodes.length < 2600) {
|
||||
return true;
|
||||
}
|
||||
if (timestamp >= 1552953600 /* 2019-03-19 */ && timestamp <= 1556323200 /* 2019-05-27 */ && graph.nodes.length < 4000) {
|
||||
return true;
|
||||
}
|
||||
if (timestamp >= 1557446400 /* 2019-05-10 */ && timestamp <= 1560470400 /* 2019-06-14 */ && graph.nodes.length < 4000) {
|
||||
return true;
|
||||
}
|
||||
if (timestamp >= 1561680000 /* 2019-06-28 */ && timestamp <= 1563148800 /* 2019-07-15 */ && graph.nodes.length < 4000) {
|
||||
return true;
|
||||
}
|
||||
if (timestamp >= 1571270400 /* 2019-11-17 */ && timestamp <= 1580601600 /* 2020-02-02 */ && graph.nodes.length < 4500) {
|
||||
return true;
|
||||
}
|
||||
if (timestamp >= 1591142400 /* 2020-06-03 */ && timestamp <= 1592006400 /* 2020-06-13 */ && graph.nodes.length < 5500) {
|
||||
return true;
|
||||
}
|
||||
if (timestamp >= 1632787200 /* 2021-09-28 */ && timestamp <= 1633564800 /* 2021-10-07 */ && graph.nodes.length < 13000) {
|
||||
return true;
|
||||
}
|
||||
if (timestamp >= 1634256000 /* 2021-10-15 */ && timestamp <= 1645401600 /* 2022-02-21 */ && graph.nodes.length < 17000) {
|
||||
return true;
|
||||
}
|
||||
if (timestamp >= 1654992000 /* 2022-06-12 */ && timestamp <= 1661472000 /* 2022-08-26 */ && graph.nodes.length < 14000) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async $cleanupIncorrectSnapshot(): Promise<void> {
|
||||
// We do not run this one automatically because those stats are not supposed to be inserted in the first
|
||||
// place, but I write them here to remind us we manually run those queries
|
||||
|
||||
// DELETE FROM lightning_stats
|
||||
// WHERE (
|
||||
// UNIX_TIMESTAMP(added) >= 1549065600 AND UNIX_TIMESTAMP(added) <= 1550620800 AND node_count < 2600 OR
|
||||
// UNIX_TIMESTAMP(added) >= 1552953600 AND UNIX_TIMESTAMP(added) <= 1556323200 AND node_count < 4000 OR
|
||||
// UNIX_TIMESTAMP(added) >= 1557446400 AND UNIX_TIMESTAMP(added) <= 1560470400 AND node_count < 4000 OR
|
||||
// UNIX_TIMESTAMP(added) >= 1561680000 AND UNIX_TIMESTAMP(added) <= 1563148800 AND node_count < 4000 OR
|
||||
// UNIX_TIMESTAMP(added) >= 1571270400 AND UNIX_TIMESTAMP(added) <= 1580601600 AND node_count < 4500 OR
|
||||
// UNIX_TIMESTAMP(added) >= 1591142400 AND UNIX_TIMESTAMP(added) <= 1592006400 AND node_count < 5500 OR
|
||||
// UNIX_TIMESTAMP(added) >= 1632787200 AND UNIX_TIMESTAMP(added) <= 1633564800 AND node_count < 13000 OR
|
||||
// UNIX_TIMESTAMP(added) >= 1634256000 AND UNIX_TIMESTAMP(added) <= 1645401600 AND node_count < 17000 OR
|
||||
// UNIX_TIMESTAMP(added) >= 1654992000 AND UNIX_TIMESTAMP(added) <= 1661472000 AND node_count < 14000
|
||||
// )
|
||||
|
||||
// DELETE FROM node_stats
|
||||
// WHERE (
|
||||
// UNIX_TIMESTAMP(added) >= 1549065600 AND UNIX_TIMESTAMP(added) <= 1550620800 OR
|
||||
// UNIX_TIMESTAMP(added) >= 1552953600 AND UNIX_TIMESTAMP(added) <= 1556323200 OR
|
||||
// UNIX_TIMESTAMP(added) >= 1557446400 AND UNIX_TIMESTAMP(added) <= 1560470400 OR
|
||||
// UNIX_TIMESTAMP(added) >= 1561680000 AND UNIX_TIMESTAMP(added) <= 1563148800 OR
|
||||
// UNIX_TIMESTAMP(added) >= 1571270400 AND UNIX_TIMESTAMP(added) <= 1580601600 OR
|
||||
// UNIX_TIMESTAMP(added) >= 1591142400 AND UNIX_TIMESTAMP(added) <= 1592006400 OR
|
||||
// UNIX_TIMESTAMP(added) >= 1632787200 AND UNIX_TIMESTAMP(added) <= 1633564800 OR
|
||||
// UNIX_TIMESTAMP(added) >= 1634256000 AND UNIX_TIMESTAMP(added) <= 1645401600 OR
|
||||
// UNIX_TIMESTAMP(added) >= 1654992000 AND UNIX_TIMESTAMP(added) <= 1661472000
|
||||
// )
|
||||
}
|
||||
}
|
||||
|
||||
export default new LightningStatsImporter;
|
||||
|
||||
119
backend/src/utils/ipcheck.js
Normal file
119
backend/src/utils/ipcheck.js
Normal file
@ -0,0 +1,119 @@
|
||||
var net = require('net');
|
||||
|
||||
var IPCheck = module.exports = function(input) {
|
||||
var self = this;
|
||||
|
||||
if (!(self instanceof IPCheck)) {
|
||||
return new IPCheck(input);
|
||||
}
|
||||
|
||||
self.input = input;
|
||||
self.parse();
|
||||
};
|
||||
|
||||
IPCheck.prototype.parse = function() {
|
||||
var self = this;
|
||||
|
||||
if (!self.input || typeof self.input !== 'string') return self.valid = false;
|
||||
|
||||
var ip;
|
||||
|
||||
var pos = self.input.lastIndexOf('/');
|
||||
if (pos !== -1) {
|
||||
ip = self.input.substring(0, pos);
|
||||
self.mask = +self.input.substring(pos + 1);
|
||||
} else {
|
||||
ip = self.input;
|
||||
self.mask = null;
|
||||
}
|
||||
|
||||
self.ipv = net.isIP(ip);
|
||||
self.valid = !!self.ipv && !isNaN(self.mask);
|
||||
|
||||
if (!self.valid) return;
|
||||
|
||||
// default mask = 32 for ipv4 and 128 for ipv6
|
||||
if (self.mask === null) self.mask = self.ipv === 4 ? 32 : 128;
|
||||
|
||||
if (self.ipv === 4) {
|
||||
// difference between ipv4 and ipv6 masks
|
||||
self.mask += 96;
|
||||
}
|
||||
|
||||
if (self.mask < 0 || self.mask > 128) {
|
||||
self.valid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
self.address = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
|
||||
|
||||
if(self.ipv === 4){
|
||||
self.parseIPv4(ip);
|
||||
}else{
|
||||
self.parseIPv6(ip);
|
||||
}
|
||||
};
|
||||
|
||||
IPCheck.prototype.parseIPv4 = function(ip) {
|
||||
var self = this;
|
||||
|
||||
// ipv4 addresses live under ::ffff:0:0
|
||||
self.address[10] = self.address[11] = 0xff;
|
||||
|
||||
var octets = ip.split('.');
|
||||
for (var i = 0; i < 4; i++) {
|
||||
self.address[i + 12] = parseInt(octets[i], 10);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var V6_TRANSITIONAL = /:(\d+\.\d+\.\d+\.\d+)$/;
|
||||
|
||||
IPCheck.prototype.parseIPv6 = function(ip) {
|
||||
var self = this;
|
||||
|
||||
var transitionalMatch = V6_TRANSITIONAL.exec(ip);
|
||||
if(transitionalMatch){
|
||||
self.parseIPv4(transitionalMatch[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
var bits = ip.split(':');
|
||||
if (bits.length < 8) {
|
||||
ip = ip.replace('::', Array(11 - bits.length).join(':'));
|
||||
bits = ip.split(':');
|
||||
}
|
||||
|
||||
var j = 0;
|
||||
for (var i = 0; i < bits.length; i += 1) {
|
||||
var x = bits[i] ? parseInt(bits[i], 16) : 0;
|
||||
self.address[j++] = x >> 8;
|
||||
self.address[j++] = x & 0xff;
|
||||
}
|
||||
};
|
||||
|
||||
IPCheck.prototype.match = function(cidr) {
|
||||
var self = this;
|
||||
|
||||
if (!(cidr instanceof IPCheck)) cidr = new IPCheck(cidr);
|
||||
if (!self.valid || !cidr.valid) return false;
|
||||
|
||||
var mask = cidr.mask;
|
||||
var i = 0;
|
||||
|
||||
while (mask >= 8) {
|
||||
if (self.address[i] !== cidr.address[i]) return false;
|
||||
|
||||
i++;
|
||||
mask -= 8;
|
||||
}
|
||||
|
||||
var shift = 8 - mask;
|
||||
return (self.address[i] >>> shift) === (cidr.address[i] >>> shift);
|
||||
};
|
||||
|
||||
|
||||
IPCheck.match = function(ip, cidr) {
|
||||
ip = ip instanceof IPCheck ? ip : new IPCheck(ip);
|
||||
return ip.match(cidr);
|
||||
};
|
||||
5
backend/testSetup.ts
Normal file
5
backend/testSetup.ts
Normal file
@ -0,0 +1,5 @@
|
||||
jest.mock('./mempool-config.json', () => ({}), { virtual: true });
|
||||
jest.mock('./src/logger.ts', () => ({}), { virtual: true });
|
||||
jest.mock('./src/api/rbf-cache.ts', () => ({}), { virtual: true });
|
||||
jest.mock('./src/api/mempool.ts', () => ({}), { virtual: true });
|
||||
jest.mock('./src/api/memory-cache.ts', () => ({}), { virtual: true });
|
||||
@ -13,7 +13,8 @@
|
||||
"node_modules/@types"
|
||||
],
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true,
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule, PreloadAllModules } from '@angular/router';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { AppPreloadingStrategy } from './app.preloading-strategy'
|
||||
import { StartComponent } from './components/start/start.component';
|
||||
import { TransactionComponent } from './components/transaction/transaction.component';
|
||||
import { TransactionPreviewComponent } from './components/transaction/transaction-preview.component';
|
||||
@ -25,6 +26,10 @@ import { AssetsComponent } from './components/assets/assets.component';
|
||||
import { AssetComponent } from './components/asset/asset.component';
|
||||
import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component';
|
||||
|
||||
const browserWindow = window || {};
|
||||
// @ts-ignore
|
||||
const browserWindowEnv = browserWindow.__env || {};
|
||||
|
||||
let routes: Routes = [
|
||||
{
|
||||
path: 'testnet',
|
||||
@ -32,7 +37,8 @@ let routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
@ -109,7 +115,8 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'docs',
|
||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
@ -117,7 +124,8 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'lightning',
|
||||
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
|
||||
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule),
|
||||
data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true },
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -410,10 +418,6 @@ let routes: Routes = [
|
||||
},
|
||||
];
|
||||
|
||||
const browserWindow = window || {};
|
||||
// @ts-ignore
|
||||
const browserWindowEnv = browserWindow.__env || {};
|
||||
|
||||
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'bisq') {
|
||||
routes = [{
|
||||
path: '',
|
||||
@ -691,7 +695,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
initialNavigation: 'enabled',
|
||||
scrollPositionRestoration: 'enabled',
|
||||
anchorScrolling: 'enabled',
|
||||
preloadingStrategy: PreloadAllModules
|
||||
preloadingStrategy: AppPreloadingStrategy
|
||||
})],
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
|
||||
@ -18,6 +18,7 @@ import { LanguageService } from './services/language.service';
|
||||
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
|
||||
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
|
||||
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
|
||||
import { AppPreloadingStrategy } from './app.preloading-strategy';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -44,6 +45,7 @@ import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe
|
||||
ShortenStringPipe,
|
||||
FiatShortenerPipe,
|
||||
CapAddressPipe,
|
||||
AppPreloadingStrategy,
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
|
||||
10
frontend/src/app/app.preloading-strategy.ts
Normal file
10
frontend/src/app/app.preloading-strategy.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { PreloadingStrategy, Route } from '@angular/router';
|
||||
import { Observable, timer, mergeMap, of } from 'rxjs';
|
||||
|
||||
export class AppPreloadingStrategy implements PreloadingStrategy {
|
||||
preload(route: Route, load: Function): Observable<any> {
|
||||
return route.data && route.data.preload
|
||||
? timer(1500).pipe(mergeMap(() => load()))
|
||||
: of(null);
|
||||
}
|
||||
}
|
||||
@ -19,6 +19,7 @@ import { AddressInformation } from 'src/app/interfaces/node-api.interface';
|
||||
export class AddressPreviewComponent implements OnInit, OnDestroy {
|
||||
network = '';
|
||||
|
||||
rawAddress: string;
|
||||
address: Address;
|
||||
addressString: string;
|
||||
isLoadingAddress = true;
|
||||
@ -55,7 +56,8 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
||||
this.mainSubscription = this.route.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.openGraphService.waitFor('address-data');
|
||||
this.rawAddress = params.get('id') || '';
|
||||
this.openGraphService.waitFor('address-data-' + this.rawAddress);
|
||||
this.error = undefined;
|
||||
this.isLoadingAddress = true;
|
||||
this.loadedConfirmedTxCount = 0;
|
||||
@ -73,7 +75,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
||||
this.isLoadingAddress = false;
|
||||
this.error = err;
|
||||
console.log(err);
|
||||
this.openGraphService.fail('address-data');
|
||||
this.openGraphService.fail('address-data-' + this.rawAddress);
|
||||
return of(null);
|
||||
})
|
||||
);
|
||||
@ -91,7 +93,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
||||
this.address = address;
|
||||
this.updateChainStats();
|
||||
this.isLoadingAddress = false;
|
||||
this.openGraphService.waitOver('address-data');
|
||||
this.openGraphService.waitOver('address-data-' + this.rawAddress);
|
||||
})
|
||||
)
|
||||
.subscribe(() => {},
|
||||
@ -99,7 +101,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
||||
console.log(error);
|
||||
this.error = error;
|
||||
this.isLoadingAddress = false;
|
||||
this.openGraphService.fail('address-data');
|
||||
this.openGraphService.fail('address-data-' + this.rawAddress);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,7 +2,36 @@
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
||||
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
|
||||
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
|
||||
<img src="/resources/bisq/bisq-markets-logo.png" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }">
|
||||
<div height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }">
|
||||
<svg width="140" viewBox="0 0 280 71" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M68.137 62.1803C68.137 66.8789 64.3484 70.6717 59.6552 70.6717H8.48178C3.78853 70.6717 0 66.8789 0 62.1803V10.9485C0 6.24988 3.8168 2.45703 8.48178 2.45703H59.6552C64.3484 2.45703 68.137 6.24988 68.137 10.9485V62.1803Z" fill="#2E3349"/>
|
||||
<path d="M0 36.6504V62.1814C0 66.88 3.8168 70.6728 8.51005 70.6728H59.6552C64.3484 70.6728 68.1652 66.88 68.1652 62.1814V36.6504H0Z" fill="url(#paint0_linear)"/>
|
||||
<path opacity="0.3" d="M60.054 61.5586C60.054 62.6059 59.3472 63.455 58.4707 63.455H49.6497C48.7732 63.455 48.0664 62.6059 48.0664 61.5586V11.5722C48.0664 10.5249 48.7732 9.67578 49.6497 9.67578H58.4707C59.3472 9.67578 60.054 10.5249 60.054 11.5722V61.5586Z" fill="white"/>
|
||||
<path d="M85.4102 33.8734H89.6242V30.6894H89.7179C91.3567 33.0774 94.447 34.4352 97.4437 34.4352C104.327 34.4352 108.728 29.3315 108.728 22.7763C108.728 16.1274 104.28 11.1173 97.4437 11.1173C94.2597 11.1173 91.2162 12.5688 89.7179 14.8632H89.6242V3.84819H85.4102V33.8734ZM96.9287 30.5021C92.4336 30.5021 89.6242 27.2713 89.6242 22.7763C89.6242 18.2812 92.4336 15.0504 96.9287 15.0504C101.424 15.0504 104.233 18.2812 104.233 22.7763C104.233 27.2713 101.424 30.5021 96.9287 30.5021Z" fill="white"/>
|
||||
<path d="M112.059 33.8734H116.274V11.6792H112.059V33.8734ZM111.076 3.71923C111.076 5.40487 112.481 6.80957 114.167 6.80957C115.852 6.80957 117.257 5.40487 117.257 3.71923C117.257 2.0336 115.852 0.628906 114.167 0.628906C112.481 0.628906 111.076 2.0336 111.076 3.71923Z" fill="white"/>
|
||||
<path d="M136.522 14.7695C134.93 12.1474 131.887 11.1173 128.937 11.1173C124.769 11.1173 120.509 13.318 120.509 17.9535C120.509 22.2144 123.693 23.385 127.298 24.2746C129.124 24.696 132.636 25.1643 132.636 27.6927C132.636 29.6125 130.295 30.5021 128.141 30.5021C125.706 30.5021 124.114 29.2379 122.756 27.88L119.572 30.5021C121.773 33.4988 124.489 34.4352 128.141 34.4352C132.542 34.4352 137.131 32.4687 137.131 27.4586C137.131 23.2913 134.321 21.8866 130.669 20.997C128.796 20.5756 125.004 20.201 125.004 17.5321C125.004 15.9401 126.736 15.0504 128.703 15.0504C130.81 15.0504 132.261 16.0337 133.244 17.2511L136.522 14.7695Z" fill="white"/>
|
||||
<path d="M162.956 11.6792H158.742V14.8632H158.648C157.01 12.4752 153.919 11.1173 150.923 11.1173C144.04 11.1173 139.638 16.221 139.638 22.7763C139.638 29.4252 144.086 34.4352 150.923 34.4352C154.107 34.4352 157.15 32.9837 158.648 30.6894H158.742V39.9435H162.956V11.6792ZM151.438 15.0504C155.933 15.0504 158.742 18.2812 158.742 22.7763C158.742 27.2713 155.933 30.5021 151.438 30.5021C146.943 30.5021 144.133 27.2713 144.133 22.7763C144.133 18.2812 146.943 15.0504 151.438 15.0504Z" fill="white"/>
|
||||
<path d="M84.8989 66.394C86.5846 66.394 87.9893 64.9893 87.9893 63.3037C87.9893 61.6181 86.5846 60.2134 84.8989 60.2134C83.2133 60.2134 81.8086 61.6181 81.8086 63.3037C81.8086 64.9893 83.2133 66.394 84.8989 66.394Z" fill="#25B135"/>
|
||||
<path d="M94.6063 66.1131H98.8204V54.5946C98.8204 49.5845 101.536 47.2902 104.58 47.2902C108.653 47.2902 109.262 50.2869 109.262 54.5009V66.1131H113.476V53.9859C113.476 50.0527 115.068 47.2902 119.142 47.2902C123.215 47.2902 123.918 50.3805 123.918 53.7518V66.1131H128.132V53.1899C128.132 48.2266 126.54 43.357 119.704 43.357C117.035 43.357 114.132 44.7617 112.68 47.4775C111.275 44.7617 109.028 43.357 105.75 43.357C101.77 43.357 99.0545 46.0728 98.6331 47.3838H98.5395V43.9189H94.6063V66.1131Z" fill="#25B135"/>
|
||||
<path d="M135.566 49.2567C137.111 48.0862 138.656 46.7283 141.887 46.7283C145.493 46.7283 147.178 49.1163 147.178 51.4106V52.3471H144.088C137.345 52.3471 131.867 54.3137 131.867 60.0261C131.867 64.3338 135.426 66.675 139.546 66.675C142.917 66.675 145.446 65.598 147.319 62.7418H147.412C147.412 63.8656 147.459 64.9893 147.553 66.1131H151.299C151.158 64.9425 151.111 63.6315 151.111 62.0863V50.7551C151.111 46.9156 148.396 43.357 141.84 43.357C138.75 43.357 135.379 44.434 133.038 46.6346L135.566 49.2567ZM147.178 55.4374V56.8421C147.178 59.8388 145.539 63.3037 140.857 63.3037C137.954 63.3037 136.081 62.2268 136.081 59.6983C136.081 56.1398 140.951 55.4374 144.931 55.4374H147.178Z" fill="#25B135"/>
|
||||
<path d="M155.689 66.1131H159.903V54.9692C159.903 50.0996 162.151 47.8521 166.271 47.8521C166.927 47.8521 167.629 47.9925 168.331 48.1798L168.519 43.638C167.957 43.4507 167.301 43.357 166.646 43.357C163.883 43.357 161.074 44.9958 159.997 47.337H159.903V43.9189H155.689V66.1131Z" fill="#25B135"/>
|
||||
<path d="M171.484 66.1131H175.698V54.5946L185.999 66.1131H191.993L180.755 54.0327L191.103 43.9657H185.25L175.698 53.5645V34.5527H171.484V66.1131Z" fill="#25B135"/>
|
||||
<path d="M215.206 56.5612V55.0628C215.206 49.3504 212.209 43.357 204.39 43.357C197.741 43.357 192.918 48.3671 192.918 55.016C192.918 61.6181 197.32 66.675 204.343 66.675C208.604 66.675 211.835 65.1766 214.176 62.1331L210.992 59.6983C209.353 61.7117 207.48 63.0228 204.905 63.0228C201.019 63.0228 197.413 60.4475 197.413 56.5612H215.206ZM197.413 53.1899C197.413 50.24 200.129 46.7283 204.296 46.7283C208.557 46.7283 210.617 49.4909 210.711 53.1899H197.413Z" fill="#25B135"/>
|
||||
<path d="M232.14 43.9189H226.1V37.6914H221.886V43.9189H217.016V47.5711H221.886V59.1364C221.886 62.695 221.98 66.675 228.488 66.675C229.331 66.675 231.297 66.4877 232.281 65.9258V62.0863C231.438 62.6014 230.267 62.7418 229.284 62.7418C226.1 62.7418 226.1 60.1197 226.1 57.6381V47.5711H232.14V43.9189Z" fill="#25B135"/>
|
||||
<path d="M252.654 47.0092C251.062 44.3871 248.019 43.357 245.069 43.357C240.902 43.357 236.641 45.5577 236.641 50.1932C236.641 54.4541 239.825 55.6247 243.43 56.5143C245.256 56.9358 248.768 57.404 248.768 59.9324C248.768 61.8522 246.427 62.7418 244.273 62.7418C241.838 62.7418 240.246 61.4776 238.888 60.1197L235.704 62.7418C237.905 65.7385 240.621 66.675 244.273 66.675C248.674 66.675 253.263 64.7084 253.263 59.6983C253.263 55.5311 250.454 54.1264 246.801 53.2367C244.929 52.8153 241.136 52.4407 241.136 49.7718C241.136 48.1798 242.868 47.2902 244.835 47.2902C246.942 47.2902 248.393 48.2735 249.377 49.4909L252.654 47.0092Z" fill="#25B135"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="34.0826" y1="36.6504" x2="34.0826" y2="77.1139" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#25B135"/>
|
||||
<stop offset="1" stop-color="#005209"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0">
|
||||
<rect width="280" height="70" fill="white" transform="translate(0 0.671875)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="connection-badge">
|
||||
<div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
|
||||
<div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div>
|
||||
@ -12,16 +41,16 @@
|
||||
|
||||
<div ngbDropdown (window:resize)="onResize($event)" class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
|
||||
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
|
||||
<img src="/resources/bisq-logo.png" style="width: 25px; height: 25px;" class="mr-1">
|
||||
<app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images>
|
||||
</button>
|
||||
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage" ngbDropdownItem class="mainnet"><img src="/resources/bitcoin-logo.png" style="width: 30px;" class="mr-1"> Mainnet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + '/signet'" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><img src="/resources/signet-logo.png" style="width: 30px;" class="mr-1"> Signet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><img src="/resources/testnet-logo.png" style="width: 30px;" class="mr-1"> Testnet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + '/signet'" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
|
||||
<h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
|
||||
<a ngbDropdownItem class="mainnet active" routerLink="/"><img src="/resources/bisq-logo.png" style="width: 30px;" class="mr-1"> Bisq</a>
|
||||
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid"><img src="/resources/liquid-logo.png" style="width: 30px;" class="mr-1"> Liquid</a>
|
||||
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet"><img src="/resources/liquidtestnet-logo.png" style="width: 30px;" class="mr-1"> Liquid Testnet</a>
|
||||
<a ngbDropdownItem class="mainnet active" routerLink="/"><app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
|
||||
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
|
||||
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
flex-direction: column;
|
||||
@media (min-width: 991px) {
|
||||
position: relative;
|
||||
top: -65px;
|
||||
top: -100px;
|
||||
}
|
||||
@media (min-width: 830px) and (max-width: 991px) {
|
||||
position: relative;
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
flex-direction: column;
|
||||
@media (min-width: 991px) {
|
||||
position: relative;
|
||||
top: -65px;
|
||||
top: -100px;
|
||||
}
|
||||
@media (min-width: 830px) and (max-width: 991px) {
|
||||
position: relative;
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
flex-direction: column;
|
||||
@media (min-width: 991px) {
|
||||
position: relative;
|
||||
top: -65px;
|
||||
top: -100px;
|
||||
}
|
||||
@media (min-width: 830px) and (max-width: 991px) {
|
||||
position: relative;
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
flex-direction: column;
|
||||
@media (min-width: 991px) {
|
||||
position: relative;
|
||||
top: -65px;
|
||||
top: -100px;
|
||||
}
|
||||
@media (min-width: 830px) and (max-width: 991px) {
|
||||
position: relative;
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
flex-direction: column;
|
||||
@media (min-width: 1130px) {
|
||||
position: relative;
|
||||
top: -65px;
|
||||
top: -100px;
|
||||
}
|
||||
@media (min-width: 830px) and (max-width: 1130px) {
|
||||
position: relative;
|
||||
|
||||
@ -20,6 +20,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
||||
block: BlockExtended;
|
||||
blockHeight: number;
|
||||
blockHash: string;
|
||||
rawId: string;
|
||||
isLoadingBlock = true;
|
||||
strippedTransactions: TransactionStripped[];
|
||||
overviewTransitionDirection: string;
|
||||
@ -48,8 +49,9 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
||||
|
||||
const block$ = this.route.paramMap.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.openGraphService.waitFor('block-viz');
|
||||
this.openGraphService.waitFor('block-data');
|
||||
this.rawId = params.get('id') || '';
|
||||
this.openGraphService.waitFor('block-viz-' + this.rawId);
|
||||
this.openGraphService.waitFor('block-data-' + this.rawId);
|
||||
|
||||
const blockHash: string = params.get('id') || '';
|
||||
this.block = undefined;
|
||||
@ -80,8 +82,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
||||
}),
|
||||
catchError((err) => {
|
||||
this.error = err;
|
||||
this.openGraphService.fail('block-data');
|
||||
this.openGraphService.fail('block-viz');
|
||||
this.openGraphService.fail('block-data-' + this.rawId);
|
||||
this.openGraphService.fail('block-viz-' + this.rawId);
|
||||
return of(null);
|
||||
}),
|
||||
);
|
||||
@ -103,7 +105,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
||||
this.isLoadingOverview = true;
|
||||
this.overviewError = null;
|
||||
|
||||
this.openGraphService.waitOver('block-data');
|
||||
this.openGraphService.waitOver('block-data-' + this.rawId);
|
||||
}),
|
||||
throttleTime(50, asyncScheduler, { leading: true, trailing: true }),
|
||||
shareReplay(1)
|
||||
@ -116,7 +118,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
this.overviewError = err;
|
||||
this.openGraphService.fail('block-viz');
|
||||
this.openGraphService.fail('block-viz-' + this.rawId);
|
||||
return of([]);
|
||||
}),
|
||||
switchMap((transactions) => {
|
||||
@ -136,8 +138,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
||||
(error) => {
|
||||
this.error = error;
|
||||
this.isLoadingOverview = false;
|
||||
this.openGraphService.fail('block-viz');
|
||||
this.openGraphService.fail('block-data');
|
||||
this.openGraphService.fail('block-viz-' + this.rawId);
|
||||
this.openGraphService.fail('block-data-' + this.rawId);
|
||||
if (this.blockGraph) {
|
||||
this.blockGraph.destroy();
|
||||
}
|
||||
@ -163,6 +165,6 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
onGraphReady(): void {
|
||||
this.openGraphService.waitOver('block-viz');
|
||||
this.openGraphService.waitOver('block-viz-' + this.rawId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
#divider {
|
||||
width: 3px;
|
||||
width: 2px;
|
||||
height: 200px;
|
||||
left: 0;
|
||||
top: -50px;
|
||||
background-image: url('/resources/divider-new.png');
|
||||
background-repeat: repeat-y;
|
||||
position: absolute;
|
||||
margin-bottom: 120px;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<span #buttonWrapper [attr.data-tlite]="copiedMessage" style="position: relative;">
|
||||
<button #btn class="btn btn-sm btn-link pt-0" [style]="{'line-height': size === 'small' ? '0.2' : '0.8'}" [attr.data-clipboard-text]="text">
|
||||
<img src="/resources/clippy.svg" [width]="size === 'small' ? 10 : 13">
|
||||
<button #btn class="btn btn-sm btn-link pt-0 {{ leftPadding ? 'padding' : '' }}" [attr.data-clipboard-text]="text">
|
||||
<app-svg-images name="clippy" [width]="size === 'small' ? '10' : '13'" viewBox="0 0 1000 1000"></app-svg-images>
|
||||
</button>
|
||||
</span>
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
.btn-link {
|
||||
padding: 0.25rem 0 0.1rem 0.5rem;
|
||||
padding: 0.25rem 0 0.1rem 0rem;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.padding {
|
||||
padding-left: 0.4rem;
|
||||
}
|
||||
|
||||
img {
|
||||
|
||||
@ -13,6 +13,7 @@ export class ClipboardComponent implements AfterViewInit {
|
||||
@ViewChild('buttonWrapper') buttonWrapper: ElementRef;
|
||||
@Input() size: 'small' | 'normal' = 'normal';
|
||||
@Input() text: string;
|
||||
@Input() leftPadding = true;
|
||||
copiedMessage: string = $localize`:@@clipboard.copied-message:Copied!`;
|
||||
|
||||
clipboard: any;
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
|
||||
</div>
|
||||
<div class="symbol"><app-time-until [time]="epochData.remainingTime" [fastRender]="true"></app-time-until></div>
|
||||
<div class="symbol"><app-time-until [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time-until></div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
|
||||
|
||||
@ -11,7 +11,7 @@ interface EpochProgress {
|
||||
newDifficultyHeight: number;
|
||||
colorAdjustments: string;
|
||||
colorPreviousAdjustments: string;
|
||||
remainingTime: number;
|
||||
estimatedRetargetDate: number;
|
||||
previousRetarget: number;
|
||||
blocksUntilHalving: number;
|
||||
timeUntilHalving: number;
|
||||
@ -74,7 +74,7 @@ export class DifficultyComponent implements OnInit {
|
||||
colorAdjustments,
|
||||
colorPreviousAdjustments,
|
||||
newDifficultyHeight: da.nextRetargetHeight,
|
||||
remainingTime: da.remainingTime,
|
||||
estimatedRetargetDate: da.estimatedRetargetDate,
|
||||
previousRetarget: da.previousRetarget,
|
||||
blocksUntilHalving,
|
||||
timeUntilHalving,
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
flex-direction: column;
|
||||
@media (min-width: 991px) {
|
||||
position: relative;
|
||||
top: -65px;
|
||||
top: -100px;
|
||||
}
|
||||
@media (min-width: 830px) and (max-width: 991px) {
|
||||
position: relative;
|
||||
|
||||
@ -55,7 +55,7 @@
|
||||
flex-direction: column;
|
||||
@media (min-width: 991px) {
|
||||
position: relative;
|
||||
top: -65px;
|
||||
top: -100px;
|
||||
}
|
||||
@media (min-width: 830px) and (max-width: 991px) {
|
||||
position: relative;
|
||||
|
||||
@ -3,7 +3,40 @@
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
||||
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
|
||||
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
|
||||
<img src="/resources/liquid/liquid-network-logo.png" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }">
|
||||
<div class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }">
|
||||
<svg width="140" viewBox="0 0 500 126" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M123.794 110.244C123.794 118.781 116.911 125.672 108.384 125.672H15.4101C6.88316 125.672 0 118.781 0 110.244V17.164C0 8.62734 6.93453 1.73633 15.4101 1.73633H108.384C116.911 1.73633 123.794 8.62734 123.794 17.164V110.244Z" fill="#2E3349"/>
|
||||
<path d="M0 63.8574V110.243C0 118.78 6.93453 125.671 15.4614 125.671H108.384C116.911 125.671 123.846 118.78 123.846 110.243V63.8574H0Z" fill="url(#paint0_linear)"/>
|
||||
<path opacity="0.3" d="M109.108 109.109C109.108 111.011 107.824 112.554 106.231 112.554H90.2047C88.6123 112.554 87.3281 111.011 87.3281 109.109V18.2912C87.3281 16.3885 88.6123 14.8457 90.2047 14.8457H106.231C107.824 14.8457 109.108 16.3885 109.108 18.2912V109.109Z" fill="white"/>
|
||||
<path d="M155.766 58.4592H163.422V5.30273H155.766V58.4592Z" fill="white"/>
|
||||
<path d="M173.413 58.4592H181.07V18.1358H173.413V58.4592Z" fill="white"/>
|
||||
<path d="M230.874 18.1358H223.217V23.9206H223.047C220.07 19.582 214.455 17.115 209.011 17.115C196.505 17.115 188.509 26.3876 188.509 38.2975C188.509 50.3775 196.59 59.48 209.011 59.48C214.796 59.48 220.325 56.8428 223.047 52.6744H223.217V70.525H230.874V18.1358ZM209.947 24.2609C218.113 24.2609 223.217 30.1307 223.217 38.2975C223.217 46.4642 218.113 52.3341 209.947 52.3341C201.78 52.3341 196.676 46.4642 196.676 38.2975C196.676 30.1307 201.78 24.2609 209.947 24.2609Z" fill="white"/>
|
||||
<path d="M275.412 18.1358H267.755V38.893C267.755 48.2507 262.906 52.3341 256.526 52.3341C251.677 52.3341 247.338 49.5268 247.338 41.9555V18.1358H239.682V44.0823C239.682 54.8011 246.488 59.48 254.314 59.48C260.524 59.48 265.629 56.5025 267.585 52.249H267.755V58.4592H275.412V18.1358Z" fill="white"/>
|
||||
<path d="M284.759 58.4592H292.415V18.1358H284.759V58.4592Z" fill="white"/>
|
||||
<path d="M340.825 5.30273H333.168V23.9206H332.998C330.276 19.7521 324.746 17.115 318.962 17.115C306.541 17.115 298.46 26.2175 298.46 38.2975C298.46 50.2073 306.456 59.48 318.962 59.48C324.406 59.48 330.021 57.013 332.998 52.6744H333.168V58.4592H340.825V5.30273ZM319.897 52.3341C311.731 52.3341 306.626 46.4642 306.626 38.2975C306.626 30.1307 311.731 24.2609 319.897 24.2609C328.064 24.2609 333.168 30.1307 333.168 38.2975C333.168 46.4642 328.064 52.3341 319.897 52.3341Z" fill="white"/>
|
||||
<path d="M177.161 9.93971C174.634 9.93971 172.527 7.83338 172.527 5.30579C172.527 2.7782 174.634 0.671875 177.161 0.671875C179.689 0.671875 181.795 2.7782 181.795 5.30579C181.795 7.83338 179.689 9.93971 177.161 9.93971Z" fill="white"/>
|
||||
<path d="M155.771 115.885C153.243 115.885 151.137 113.779 151.137 111.251C151.137 108.724 153.243 106.617 155.771 106.617C158.298 106.617 160.405 108.724 160.405 111.251C160.405 113.779 158.298 115.885 155.771 115.885Z" fill="#2CCCBF"/>
|
||||
<path d="M288.384 9.93971C285.856 9.93971 283.75 7.83338 283.75 5.30579C283.75 2.7782 285.856 0.671875 288.384 0.671875C290.912 0.671875 293.018 2.7782 293.018 5.30579C293.018 7.83338 290.912 9.93971 288.384 9.93971Z" fill="white"/>
|
||||
<path d="M166.199 115.799H173.856V95.042C173.856 85.6843 178.705 81.6009 185.085 81.6009C189.934 81.6009 194.272 84.4082 194.272 91.9795V115.799H201.929V89.8527C201.929 79.1339 195.123 74.455 187.297 74.455C181.087 74.455 175.982 77.4325 174.026 81.686H173.856V75.4758H166.199V115.799Z" fill="#2CCCBF"/>
|
||||
<path d="M247.398 98.4448V95.7226C247.398 85.344 241.953 74.455 227.746 74.455C215.666 74.455 206.904 83.5575 206.904 95.6375C206.904 107.632 214.901 116.82 227.661 116.82C235.403 116.82 241.272 114.098 245.526 108.568L239.741 104.145C236.764 107.803 233.361 110.185 228.682 110.185C221.621 110.185 215.071 105.506 215.071 98.4448H247.398ZM215.071 92.3198C215.071 86.9604 220.005 80.5801 227.576 80.5801C235.318 80.5801 239.061 85.5992 239.231 92.3198H215.071Z" fill="#2CCCBF"/>
|
||||
<path d="M275.375 75.4758H264.401V64.1615H256.745V75.4758H247.897V82.1113H256.745V103.124C256.745 109.589 256.915 116.82 268.74 116.82C270.271 116.82 273.844 116.48 275.63 115.459V108.483C274.099 109.419 271.972 109.674 270.186 109.674C264.401 109.674 264.401 104.91 264.401 100.401V82.1113H275.375V75.4758Z" fill="#2CCCBF"/>
|
||||
<path d="M293.675 115.799H301.927L311.54 85.6843H311.71L322.344 115.799H330.085L343.101 75.4758H335.02L326.342 105.591H326.172L315.964 75.4758H307.882L298.269 105.591H298.099L288.996 75.4758H280.404L293.675 115.799Z" fill="#2CCCBF"/>
|
||||
<path d="M353.225 95.6375C353.225 87.4708 358.329 81.6009 366.496 81.6009C374.663 81.6009 379.767 87.4708 379.767 95.6375C379.767 103.804 374.663 109.674 366.496 109.674C358.329 109.674 353.225 103.804 353.225 95.6375ZM345.058 95.6375C345.058 107.377 354.501 116.82 366.496 116.82C378.491 116.82 387.933 107.377 387.933 95.6375C387.933 83.8978 378.491 74.455 366.496 74.455C354.501 74.455 345.058 83.8978 345.058 95.6375Z" fill="#2CCCBF"/>
|
||||
<path d="M394.736 115.799H402.392V95.5525C402.392 86.7051 406.476 82.6218 413.962 82.6218C415.153 82.6218 416.429 82.877 417.705 83.2172L418.045 74.9654C417.024 74.6251 415.833 74.455 414.642 74.455C409.623 74.455 404.519 77.4325 402.562 81.686H402.392V75.4758H394.736V115.799Z" fill="#2CCCBF"/>
|
||||
<path d="M423.433 115.799H431.089V94.8719L449.805 115.799H460.694L440.277 93.8511L459.077 75.5609H448.443L431.089 93.0003V58.459H423.433V115.799Z" fill="#2CCCBF"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="61.9228" y1="63.8574" x2="61.9228" y2="137.373" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#64DED2"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0">
|
||||
<rect width="500" height="125" fill="white" transform="translate(0 0.671875)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="connection-badge">
|
||||
<div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
|
||||
<div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div>
|
||||
@ -13,16 +46,16 @@
|
||||
|
||||
<div ngbDropdown (window:resize)="onResize($event)" class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
|
||||
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
|
||||
<img src="/resources/{{ network.val === '' ? 'liquid' : network.val }}-logo.png" style="width: 25px; height: 25px;" class="mr-1">
|
||||
<app-svg-images [name]="network.val === '' ? 'liquid' : network.val" width="22" height="22" viewBox="0 0 125 125" style="width: 30px; height: 30px; margin-right: 5px;"></app-svg-images>
|
||||
</button>
|
||||
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage" ngbDropdownItem class="mainnet"><img src="/resources/bitcoin-logo.png" style="width: 30px;" class="mr-1"> Mainnet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + '/signet'" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><img src="/resources/signet-logo.png" style="width: 30px;" class="mr-1"> Signet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><img src="/resources/testnet-logo.png" style="width: 30px;" class="mr-1"> Testnet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + '/signet'" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
|
||||
<h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
|
||||
<a [href]="env.BISQ_WEBSITE_URL + urlLanguage" ngbDropdownItem class="mainnet"><img src="/resources/bisq-logo.png" style="width: 30px;" class="mr-1"> Bisq</a>
|
||||
<a ngbDropdownItem class="liquid mr-1" [class.active]="network.val === 'liquid'" routerLink="/"><img src="/resources/liquid-logo.png" style="width: 30px;"> Liquid</a>
|
||||
<a ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquidtestnet'" routerLink="/testnet"><img src="/resources/liquidtestnet-logo.png" style="width: 30px;" class="mr-1"> Liquid Testnet</a>
|
||||
<a [href]="env.BISQ_WEBSITE_URL + urlLanguage" ngbDropdownItem class="mainnet"><app-svg-images name="bisq" width="22" height="22" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
|
||||
<a ngbDropdownItem class="liquid mr-1" [class.active]="network.val === 'liquid'" routerLink="/"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
|
||||
<a ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquidtestnet'" routerLink="/testnet"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -2,17 +2,17 @@
|
||||
<div class="preview-wrapper">
|
||||
<header>
|
||||
<span class="header-brand" style="position: relative;">
|
||||
<img *ngIf="!officialMempoolSpace" src="/resources/mempool-logo.png" height="50" width="200" class="logo">
|
||||
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" width="500" height="126" class="mempool-logo" style="width: 200px; height: 50px"></app-svg-images>
|
||||
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" style="width: 200px; height: 50px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
|
||||
</span>
|
||||
|
||||
<div [ngSwitch]="network.val">
|
||||
<span *ngSwitchCase="'signet'" class="network signet"><img src="/resources/signet-logo.png" style="width: 45px;" class="signet mr-1" alt="logo"> Signet <ng-template [ngIf]="(lightning$ | async)">Lightning</ng-template></span>
|
||||
<span *ngSwitchCase="'testnet'" class="network testnet"><img src="/resources/testnet-logo.png" style="width: 45px;" class="mr-1" alt="testnet logo"> Testnet <ng-template [ngIf]="(lightning$ | async)">Lightning</ng-template></span>
|
||||
<span *ngSwitchCase="'bisq'" class="network bisq"><img src="/resources/bisq-logo.png" style="width: 45px;" class="mr-1" alt="bisq logo"> Bisq</span>
|
||||
<span *ngSwitchCase="'liquid'" class="network liquid"><img src="/resources/liquid-logo.png" style="width: 45px;" class="mr-1" alt="liquid mainnet logo"> Liquid</span>
|
||||
<span *ngSwitchCase="'liquidtestnet'" class="network liquidtestnet"><img src="/resources/liquidtestnet-logo.png" style="width: 45px;" class="mr-1" alt="liquid testnet logo"> Liquid Testnet</span>
|
||||
<span *ngSwitchDefault class="network mainnet"><img src="/resources/bitcoin-logo.png" style="width: 45px;" class="mainnet mr-1" alt="bitcoin logo"> Mainnet <ng-template [ngIf]="(lightning$ | async)">Lightning</ng-template></span>
|
||||
<span *ngSwitchCase="'signet'" class="network signet"><app-svg-images name="signet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Signet <ng-template [ngIf]="(lightning$ | async)">Lightning</ng-template></span>
|
||||
<span *ngSwitchCase="'testnet'" class="network testnet"><app-svg-images name="testnet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet <ng-template [ngIf]="(lightning$ | async)">Lightning</ng-template></span>
|
||||
<span *ngSwitchCase="'bisq'" class="network bisq"><app-svg-images name="bisq" width="35" height="35" viewBox="0 0 75 75" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Bisq</span>
|
||||
<span *ngSwitchCase="'liquid'" class="network liquid"><app-svg-images name="liquid" width="35" height="35" viewBox="0 0 125 125" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Liquid</span>
|
||||
<span *ngSwitchCase="'liquidtestnet'" class="network liquidtestnet"><app-svg-images name="liquidtestnet" width="35" height="35" viewBox="0 0 125 125" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</span>
|
||||
<span *ngSwitchDefault class="network mainnet"><app-svg-images name="bitcoin" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Mainnet <ng-template [ngIf]="(lightning$ | async)">Lightning</ng-template></span>
|
||||
</div>
|
||||
</header>
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
|
||||
<img *ngIf="!officialMempoolSpace" src="/resources/mempool-logo.png" class="mempool-logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }" alt="The Mempool Open Source Project logo">
|
||||
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" class="mempool-logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }"></app-svg-images>
|
||||
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>
|
||||
<div class="connection-badge">
|
||||
<div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
|
||||
@ -19,16 +19,16 @@
|
||||
|
||||
<div (window:resize)="onResize($event)" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
|
||||
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
|
||||
<img src="/resources/{{ network.val === '' ? 'bitcoin' : network.val }}-logo.png" style="width: 25px; height: 25px;" class="mr-1" [alt]="(network.val === '' ? 'bitcoin' : network.val) + ' logo'">
|
||||
<app-svg-images [name]="network.val === '' ? 'bitcoin' : network.val" width="20" height="20" viewBox="0 0 65 65" style="width: 30px; height: 30px; margin-right: 5px;"></app-svg-images>
|
||||
</button>
|
||||
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
|
||||
<a ngbDropdownItem class="mainnet" routerLink="/"><img src="/resources/bitcoin-logo.png" style="width: 30px;" class="mainnet mr-1" alt="bitcoin logo"> Mainnet</a>
|
||||
<a ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" routerLink="/signet"><img src="/resources/signet-logo.png" style="width: 30px;" class="signet mr-1" alt="logo"> Signet</a>
|
||||
<a ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" routerLink="/testnet"><img src="/resources/testnet-logo.png" style="width: 30px;" class="mr-1" alt="testnet logo"> Testnet</a>
|
||||
<a ngbDropdownItem class="mainnet" routerLink="/"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
|
||||
<a ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" routerLink="/signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
|
||||
<a ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" routerLink="/testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
|
||||
<h6 *ngIf="env.LIQUID_ENABLED || env.BISQ_ENABLED" class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
|
||||
<a [href]="env.BISQ_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.BISQ_ENABLED" class="bisq"><img src="/resources/bisq-logo.png" style="width: 30px;" class="mr-1" alt="bisq logo"> Bisq</a>
|
||||
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid" [class.active]="network.val === 'liquid'"><img src="/resources/liquid-logo.png" style="width: 30px;" class="mr-1" alt="liquid mainnet logo"> Liquid</a>
|
||||
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquid'"><img src="/resources/liquidtestnet-logo.png" style="width: 30px;" class="mr-1" alt="liquid testnet logo"> Liquid Testnet</a>
|
||||
<a [href]="env.BISQ_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.BISQ_ENABLED" class="bisq"><app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
|
||||
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid" [class.active]="network.val === 'liquid'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
|
||||
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquid'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
flex-direction: column;
|
||||
@media (min-width: 991px) {
|
||||
position: relative;
|
||||
top: -65px;
|
||||
top: -100px;
|
||||
}
|
||||
@media (min-width: 830px) and (max-width: 991px) {
|
||||
position: relative;
|
||||
|
||||
@ -1,13 +1,19 @@
|
||||
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
|
||||
<div class="d-flex">
|
||||
<div class="search-box-container mr-2">
|
||||
<input (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="TXID, block height, hash or address">
|
||||
<input (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Search the full Bitcoin ecosystem">
|
||||
|
||||
<app-search-results #searchResults [results]="typeAhead$ | async" [searchTerm]="searchForm.get('searchText').value" (selectedResult)="selectedResult($event)"></app-search-results>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<button [disabled]="isSearching" type="submit" class="btn btn-block btn-primary"><fa-icon [icon]="['fas', 'search']" [fixedWidth]="true" i18n-title="search-form.search-title" title="Search"></fa-icon></button>
|
||||
<button [disabled]="isSearching" type="submit" class="btn btn-block btn-primary">
|
||||
<fa-icon *ngIf="!(isTypeaheading$ | async) else searchLoading" [icon]="['fas', 'search']" [fixedWidth]="true" i18n-title="search-form.search-title" title="Search"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ng-template #searchLoading>
|
||||
<div class="spinner-border spinner-border-sm text-light" role="status" aria-hidden="true" (click)="searchForm.valid && search()"></div>
|
||||
</ng-template>
|
||||
|
||||
@ -49,4 +49,10 @@ form {
|
||||
.btn {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spinner-border {
|
||||
vertical-align: text-top;
|
||||
margin-top: 1px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
@ -3,8 +3,8 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { AssetsService } from 'src/app/services/assets.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { Observable, of, Subject, merge, zip } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, switchMap, filter, catchError, map } from 'rxjs/operators';
|
||||
import { Observable, of, Subject, zip, BehaviorSubject } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, switchMap, catchError, map } from 'rxjs/operators';
|
||||
import { ElectrsApiService } from 'src/app/services/electrs-api.service';
|
||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
@ -20,13 +20,14 @@ export class SearchFormComponent implements OnInit {
|
||||
network = '';
|
||||
assets: object = {};
|
||||
isSearching = false;
|
||||
isTypeaheading$ = new BehaviorSubject<boolean>(false);
|
||||
typeAhead$: Observable<any>;
|
||||
searchForm: FormGroup;
|
||||
|
||||
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/;
|
||||
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
|
||||
regexTransaction = /^([a-fA-F0-9]{64}):?(\d+)?$/;
|
||||
regexBlockheight = /^[0-9]+$/;
|
||||
regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
|
||||
regexBlockheight = /^[0-9]{1,9}$/;
|
||||
focus$ = new Subject<string>();
|
||||
click$ = new Subject<string>();
|
||||
|
||||
@ -68,7 +69,7 @@ export class SearchFormComponent implements OnInit {
|
||||
}
|
||||
return text.trim();
|
||||
}),
|
||||
debounceTime(250),
|
||||
debounceTime(200),
|
||||
distinctUntilChanged(),
|
||||
switchMap((text) => {
|
||||
if (!text.length) {
|
||||
@ -80,6 +81,7 @@ export class SearchFormComponent implements OnInit {
|
||||
}
|
||||
]);
|
||||
}
|
||||
this.isTypeaheading$.next(true);
|
||||
if (!this.stateService.env.LIGHTNING) {
|
||||
return zip(
|
||||
this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))),
|
||||
@ -95,6 +97,7 @@ export class SearchFormComponent implements OnInit {
|
||||
);
|
||||
}),
|
||||
map((result: any[]) => {
|
||||
this.isTypeaheading$.next(false);
|
||||
if (this.network === 'bisq') {
|
||||
return result[0].map((address: string) => 'B' + address);
|
||||
}
|
||||
@ -153,6 +156,7 @@ export class SearchFormComponent implements OnInit {
|
||||
this.navigate('/tx/', matches[0]);
|
||||
}
|
||||
} else {
|
||||
this.searchResults.searchButtonClick();
|
||||
this.isSearching = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<ng-template [ngIf]="results.nodes.length">
|
||||
<div class="card-title">Lightning Nodes</div>
|
||||
<ng-template ngFor [ngForOf]="results.nodes" let-node let-i="index">
|
||||
<button (click)="clickItem(results.addresses.length + i)" [class.active]="results.addresses.length + i === activeIdx" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]" type="button" role="option" class="dropdown-item">
|
||||
<button (click)="clickItem(results.addresses.length + i)" [class.inactive]="node.status === 0" [class.active]="results.addresses.length + i === activeIdx" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]" type="button" role="option" class="dropdown-item">
|
||||
<ngb-highlight [result]="node.alias" [term]="searchTerm"></ngb-highlight> <span class="symbol">{{ node.public_key | shortenString : 10 }}</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
@ -18,7 +18,7 @@
|
||||
<ng-template [ngIf]="results.channels.length">
|
||||
<div class="card-title">Lightning Channels</div>
|
||||
<ng-template ngFor [ngForOf]="results.channels" let-channel let-i="index">
|
||||
<button (click)="clickItem(results.addresses.length + results.nodes.length + i)" [class.active]="results.addresses.length + results.nodes.length + i === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<button (click)="clickItem(results.addresses.length + results.nodes.length + i)" [class.inactive]="channel.status === 2" [class.active]="results.addresses.length + results.nodes.length + i === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ngb-highlight [result]="channel.short_id" [term]="searchTerm"></ngb-highlight> <span class="symbol">{{ channel.id }}</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
@ -14,3 +14,7 @@
|
||||
box-shadow: 0.125rem 0.125rem 0.25rem rgba(0,0,0,0.075);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inactive {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
@ -25,6 +25,13 @@ export class SearchResultsComponent implements OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
searchButtonClick() {
|
||||
if (this.resultsFlattened[this.activeIdx]) {
|
||||
this.selectedResult.emit(this.resultsFlattened[this.activeIdx]);
|
||||
this.results = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown(event: KeyboardEvent) {
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
</div>
|
||||
|
||||
<form [formGroup]="radioGroupForm" class="formRadioGroup"
|
||||
[class]="stateService.env.MINING_DASHBOARD ? 'mining' : ''" (click)="saveGraphPreference()">
|
||||
[class]="(stateService.env.MINING_DASHBOARD || stateService.env.LIGHTNING) ? 'mining' : 'no-menu'" (click)="saveGraphPreference()">
|
||||
<div *ngIf="!isMobile()" class="btn-group btn-group-toggle">
|
||||
<label ngbButtonLabel class="btn-primary btn-sm mr-2">
|
||||
<a [routerLink]="['/tv' | relativeUrl]" style="color: white" id="btn-tv">
|
||||
|
||||
@ -55,13 +55,19 @@
|
||||
.formRadioGroup.mining {
|
||||
@media (min-width: 991px) {
|
||||
position: relative;
|
||||
top: -65px;
|
||||
top: -100px;
|
||||
}
|
||||
@media (min-width: 830px) and (max-width: 991px) {
|
||||
position: relative;
|
||||
top: 0px;
|
||||
}
|
||||
}
|
||||
.formRadioGroup.no-menu {
|
||||
@media (min-width: 991px) {
|
||||
position: relative;
|
||||
top: -33px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading{
|
||||
display: flex;
|
||||
|
||||
@ -25,4 +25,83 @@
|
||||
</defs>
|
||||
</svg>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'mempoolSpace'">
|
||||
<svg [class]="class" [style]="style" [attr.width]="width" [attr.height]="height" [attr.viewBox]="viewBox" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M 219.548 86.198 L 219.548 63.833 C 219.548 60.359 218.746 57.686 217.163 55.919 C 215.601 54.151 213.237 53.267 210.195 53.267 C 206.762 53.267 203.946 54.377 202.013 56.453 C 200.081 58.55 199.053 61.633 199.053 65.395 L 199.053 86.219 L 191.447 86.219 L 191.447 63.833 C 191.447 56.823 188.282 53.267 182.032 53.267 C 178.6 53.267 175.783 54.377 173.851 56.453 C 171.919 58.55 170.891 61.633 170.891 65.395 L 170.891 86.219 L 163.285 86.219 L 163.285 46.422 L 170.685 46.422 L 170.685 50.759 C 173.687 47.799 178.003 46.175 182.999 46.175 C 188.96 46.175 193.667 48.498 196.36 52.753 C 199.608 48.559 204.85 46.175 210.955 46.175 C 215.93 46.175 219.877 47.614 222.693 50.43 C 225.632 53.39 227.174 57.871 227.154 63.36 L 227.154 86.198 L 219.548 86.198 Z" fill="white"/>
|
||||
<path d="M 255.397 86.568 C 243.145 86.568 234.594 78.243 234.594 66.32 C 234.594 54.644 242.775 46.175 254.04 46.175 C 265.387 46.175 273.342 54.459 273.383 66.32 C 273.383 66.443 273.363 66.587 273.363 66.711 C 273.342 66.834 273.342 66.937 273.342 67.04 L 273.342 67.101 L 273.322 67.163 C 273.281 67.368 273.281 67.595 273.281 67.862 L 273.281 69.157 L 242.343 69.157 C 243.412 75.488 248.49 79.538 255.5 79.538 C 259.796 79.538 263.393 77.996 265.901 75.098 L 266.888 74.008 L 271.41 79.168 L 270.732 80.052 C 267.196 84.245 261.749 86.568 255.397 86.568 Z M 265.798 62.846 C 265.284 60.05 263.989 57.666 262.036 55.919 C 259.919 54.027 257.144 53.041 254.061 53.041 C 250.998 53.041 248.264 54.027 246.146 55.919 C 244.194 57.666 242.919 60.05 242.405 62.846 L 265.798 62.846 Z" fill="white"/>
|
||||
<path d="M 337.274 86.198 L 337.274 63.833 C 337.274 60.359 336.473 57.686 334.89 55.919 C 333.328 54.151 330.964 53.267 327.921 53.267 C 324.488 53.267 321.672 54.377 319.74 56.453 C 317.808 58.55 316.78 61.633 316.78 65.395 L 316.78 86.219 L 309.174 86.219 L 309.174 63.833 C 309.174 56.823 306.008 53.267 299.759 53.267 C 296.326 53.267 293.51 54.377 291.578 56.453 C 289.645 58.55 288.618 61.633 288.618 65.395 L 288.618 86.219 L 281.012 86.219 L 281.012 46.422 L 288.412 46.422 L 288.412 50.759 C 291.413 47.778 295.73 46.154 300.725 46.154 C 306.687 46.154 311.394 48.477 314.087 52.732 C 317.335 48.539 322.577 46.154 328.682 46.154 C 333.656 46.154 337.603 47.593 340.419 50.41 C 343.359 53.37 344.901 57.851 344.88 63.339 L 344.88 86.177 L 337.274 86.177 L 337.274 86.198 Z" fill="white"/>
|
||||
<path d="M 354.297 99.93 L 354.297 46.422 L 361.697 46.422 L 361.697 51.314 C 364.986 47.943 369.467 46.154 374.75 46.154 C 386.241 46.154 394.567 54.624 394.567 66.3 C 394.567 78.037 386.241 86.547 374.75 86.547 C 369.57 86.547 365.171 84.841 361.923 81.573 L 361.923 99.909 L 354.297 99.909 L 354.297 99.93 Z M 374.422 53.102 C 367.021 53.102 361.841 58.529 361.841 66.32 C 361.841 74.111 367.021 79.538 374.422 79.538 C 381.822 79.538 387.002 74.111 387.002 66.32 C 387.002 58.653 381.719 53.102 374.422 53.102 Z" fill="white"/>
|
||||
<path d="M 420.321 86.568 C 408.604 86.568 400.094 78.058 400.094 66.32 C 400.094 54.644 408.604 46.175 420.321 46.175 C 425.995 46.175 431.134 48.21 434.813 51.91 C 438.472 55.59 440.466 60.708 440.446 66.341 C 440.446 78.058 431.977 86.568 420.321 86.568 Z M 420.321 53.102 C 412.921 53.102 407.741 58.529 407.741 66.32 C 407.741 74.111 412.921 79.538 420.321 79.538 C 427.66 79.538 432.799 74.111 432.799 66.32 C 432.778 58.529 427.66 53.102 420.321 53.102 Z" fill="white"/>
|
||||
<path d="M 464.598 86.568 C 452.881 86.568 444.371 78.058 444.371 66.32 C 444.371 54.644 452.881 46.175 464.598 46.175 C 476.254 46.175 484.723 54.644 484.723 66.32 C 484.723 78.058 476.254 86.568 464.598 86.568 Z M 464.598 53.102 C 457.198 53.102 452.018 58.529 452.018 66.32 C 452.018 74.111 457.198 79.538 464.598 79.538 C 471.937 79.538 477.076 74.111 477.076 66.32 C 477.076 58.529 471.958 53.102 464.598 53.102 Z" fill="white"/>
|
||||
<path d="M 499.996 31.148 L 492.391 31.148 L 492.391 86.198 L 499.996 86.198 L 499.996 31.148 Z" fill="white"/>
|
||||
<path d="M124.706 110.25C124.706 118.849 117.772 125.791 109.183 125.791H15.5236C6.93387 125.791 0 118.849 0 110.25V16.4837C0 7.88416 6.98561 0.942383 15.5236 0.942383H109.183C117.772 0.942383 124.706 7.88416 124.706 16.4837V110.25Z" fill="#2E3349"/>
|
||||
<path d="M0 63.5225V110.25C0 118.849 6.98561 125.791 15.5753 125.791H109.183C117.772 125.791 124.758 118.849 124.758 110.25V63.5225H0Z" fill="url('#paint0_linear')"/>
|
||||
<path opacity="0.3" d="M109.909 109.11C109.909 111.026 108.615 112.581 107.011 112.581H90.8665C89.2624 112.581 87.9688 111.026 87.9688 109.11V17.6232C87.9688 15.7065 89.2624 14.1523 90.8665 14.1523H107.011C108.615 14.1523 109.909 15.7065 109.909 17.6232V109.11Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="62.3768" y1="36.3949" x2="62.3768" y2="156.837" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#AE61FF"/>
|
||||
<stop offset="1" stop-color="#13EFD8"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'bitcoin'">
|
||||
<ng-component *ngTemplateOutlet="bitcoinLogo; context: {$implicit: '#f7931a', width, height, viewBox}"></ng-component>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'signet'">
|
||||
<ng-component *ngTemplateOutlet="bitcoinLogo; context: {$implicit: '#b028aa', width, height, viewBox}"></ng-component>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'testnet'">
|
||||
<ng-component *ngTemplateOutlet="bitcoinLogo; context: {$implicit: '#5fd15c', width, height, viewBox}"></ng-component>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'bisq'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" [attr.viewBox]="viewBox"><path id="Combined-Shape" fill="#25b135" d="M62.6 18.2c2.6 5.7 3.2 6.8 5.2 12.6 1.1 3.6-1.2 9-10.6 4.4-6.8-3.2-11.7-5.5-7.6-10.9 2-2.6 4.2-5 6.6-7.3 2.5-2.2 4.1-3.6 6.4 1.2zm-2.3 36.4c5-4.9 11.9.6 7.1 7.1C57.6 75 40.7 80.3 25.5 74.9S0 55 0 38.3c0-10 3.3-18.4 9.6-25.9C15.7 5.6 24.8.1 33.8 0 46.4 0 52 11 47.6 17.2c-5 6.8-10.7 6.3-15 3.3 0 0-5.9-4.2-7.6-5.6-2.7-2.1-5.6-2.1-6.7 1.3-1.9 6-2.8 9.8-4.3 15.8-.7 3-1 6.1-1.1 9.2 0 5.7 2.5 11.1 6.8 14.6 3 2.8 6.1 5.3 9.5 7.5 2.7 1.9 5.9 2.9 9.1 2.9h.3c3.2-.1 6.4-1.1 9.1-2.9 4.4-2.6 8.6-5.5 12.6-8.7zm-35.4-7c-1.8-.7-5.8-4.2-4.6-6.8.5-1.4 3.9-1.8 5-1.8 6.2-.1 6.5 11.1-.4 8.6zm11.1 12c-.8-.7-2.3-2.2-3.6-3.4-.4-.4-.4-1-.2-1.4.3-.5.8-.8 1.4-.7 2.4-.1 4.8-.1 7.1 0 .6 0 1 .4 1.2.8.2.5.1 1-.3 1.3-1.3 1.2-2.8 2.7-3.5 3.4-.3.3-.7.4-1.1.4-.4 0-.7-.1-1-.4zm14.1-12c-6.9 2.5-6.7-8.8-.4-8.6 1.1 0 4.4.4 5 1.8 1.3 2.7-2.8 6.2-4.6 6.8z"/></svg>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'liquid'">
|
||||
<ng-component *ngTemplateOutlet="liquidLogo; context: {$implicit: '', width, height, viewBox, color1: '#2cccbf', color2: '#9ef2ed'}"></ng-component>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'liquidtestnet'">
|
||||
<ng-component *ngTemplateOutlet="liquidLogo; context: {$implicit: '', width, height, viewBox, color1: '#858585', color2: '#c6c6c6'}"></ng-component>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'clippy'">
|
||||
<svg [attr.width]="width" [attr.height]="height" [attr.viewBox]="viewBox" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#FFFFFF" d="M128 768h256v64H128v-64z m320-384H128v64h320v-64z m128 192V448L384 640l192 192V704h320V576H576z m-288-64H128v64h160v-64zM128 704h160v-64H128v64z m576 64h64v128c-1 18-7 33-19 45s-27 18-45 19H64c-35 0-64-29-64-64V192c0-35 29-64 64-64h192C256 57 313 0 384 0s128 57 128 128h192c35 0 64 29 64 64v320h-64V320H64v576h640V768zM128 256h512c0-35-29-64-64-64h-64c-35 0-64-29-64-64s-29-64-64-64-64 29-64 64-29 64-64 64h-64c-35 0-64 29-64 64z" />
|
||||
</svg>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #bitcoinLogo let-color let-width="width" let-height="height" let-viewBox="viewBox">
|
||||
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" [attr.viewBox]="viewBox" [attr.width]="width" [attr.height]="height" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<g transform="translate(0.00630876,-0.00301984)">
|
||||
<path [attr.fill]="color" d="m63.033,39.744c-4.274,17.143-21.637,27.576-38.782,23.301-17.138-4.274-27.571-21.638-23.295-38.78,4.272-17.145,21.635-27.579,38.775-23.305,17.144,4.274,27.576,21.64,23.302,38.784z"/>
|
||||
<path fill="#FFF" d="m46.103,27.444c0.637-4.258-2.605-6.547-7.038-8.074l1.438-5.768-3.511-0.875-1.4,5.616c-0.923-0.23-1.871-0.447-2.813-0.662l1.41-5.653-3.509-0.875-1.439,5.766c-0.764-0.174-1.514-0.346-2.242-0.527l0.004-0.018-4.842-1.209-0.934,3.75s2.605,0.597,2.55,0.634c1.422,0.355,1.679,1.296,1.636,2.042l-1.638,6.571c0.098,0.025,0.225,0.061,0.365,0.117-0.117-0.029-0.242-0.061-0.371-0.092l-2.296,9.205c-0.174,0.432-0.615,1.08-1.609,0.834,0.035,0.051-2.552-0.637-2.552-0.637l-1.743,4.019,4.569,1.139c0.85,0.213,1.683,0.436,2.503,0.646l-1.453,5.834,3.507,0.875,1.439-5.772c0.958,0.26,1.888,0.5,2.798,0.726l-1.434,5.745,3.511,0.875,1.453-5.823c5.987,1.133,10.489,0.676,12.384-4.739,1.527-4.36-0.076-6.875-3.226-8.515,2.294-0.529,4.022-2.038,4.483-5.155zm-8.022,11.249c-1.085,4.36-8.426,2.003-10.806,1.412l1.928-7.729c2.38,0.594,10.012,1.77,8.878,6.317zm1.086-11.312c-0.99,3.966-7.1,1.951-9.082,1.457l1.748-7.01c1.982,0.494,8.365,1.416,7.334,5.553z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #liquidLogo let-width="width" let-height="height" let-viewBox="viewBox" let-color1="color1" let-color2="color2">
|
||||
<svg [attr.width]="width" [attr.height]="height" [attr.viewBox]="viewBox" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<polygon id="path-1" points="0 0.649692716 52.8225298 0.649692716 52.8225298 106.004141 0 106.004141"/>
|
||||
</defs>
|
||||
<g id="Unified-Headers" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Assets" transform="translate(-190.000000, -200.000000)">
|
||||
<g id="brand/products/logos/liquid" transform="translate(190.000000, 200.000000)">
|
||||
<g id="Group-38" transform="translate(0.000000, -0.000000)">
|
||||
<path d="M23.1161265,84.4720497 C20.9217995,84.4720497 18.8355937,83.190767 17.9227679,81.0564707 C14.8209325,73.8058557 13.2505176,66.1022975 13.2505176,58.160812 C13.2505176,28.6260995 35.4507963,3.63844273 64.8916448,0.0413340043 C67.9881627,-0.330537599 70.7975393,1.84781928 71.178622,4.92325031 C71.5597046,7.99691891 69.3600602,10.7956493 66.2688597,11.1728081 C42.4733515,14.0825713 24.5305634,34.2817203 24.5305634,58.160812 C24.5305634,64.5866123 25.8014296,70.8132586 28.3041676,76.6662709 C29.523632,79.5161116 28.1871841,82.8118457 25.3210883,84.0226314 C24.59969,84.3275309 23.8517046,84.4720497 23.1161265,84.4720497" id="Fill-1" [attr.fill]="color1"/>
|
||||
<path d="M35.3026907,53.8302277 C34.9875568,53.8302277 34.6653808,53.791147 34.3467259,53.7076562 C32.2974754,53.1765128 31.0651083,51.0697032 31.5915051,49.0037509 C36.4646368,29.9003529 53.5346821,16.563147 73.1064326,16.563147 C83.0921274,16.563147 92.8137435,20.1017344 100.480828,26.5305234 C102.107552,27.8912453 102.331139,30.3284651 100.979056,31.9680817 C99.6304943,33.6094746 97.2150547,33.8350773 95.5883301,32.4725791 C89.2926946,27.1948968 81.3087161,24.2869311 73.1064326,24.2869311 C57.0293232,24.2869311 43.0085063,35.2419856 39.0068342,50.9275913 C38.563182,52.6737918 37.0068784,53.8302277 35.3026907,53.8302277" id="Fill-3" [attr.fill]="color2"/>
|
||||
<path d="M70.2960951,99.2782606 C65.7440202,98.9692101 61.3042321,97.9493436 57.1014942,96.2404763 C55.1124121,95.4314912 54.1428237,93.1317921 54.9341789,91.1029667 C55.7290988,89.0759592 57.981966,88.0869978 59.9710481,88.8941649 C63.4198597,90.2976176 67.0665102,91.1356897 70.811189,91.3902019 C75.981733,91.741065 81.0382078,90.9738927 85.8415915,89.1068643 C87.8431499,88.3324202 90.0817584,89.3559226 90.8428141,91.3956557 C91.6038697,93.4353888 90.600417,95.7205443 88.6006409,96.4986243 C82.7456815,98.7710542 76.5895081,99.7054774 70.2960951,99.2782606" id="Fill-5" [attr.fill]="color2"/>
|
||||
<g id="Group-9" transform="translate(0.000000, 18.219462)">
|
||||
<mask id="mask-2" fill="white">
|
||||
<polygon points="0 0.649692716 52.8225298 0.649692716 52.8225298 106.004141 0 106.004141" transform="matrix(1, 0, 0, 1, 0, 0)"/>
|
||||
</mask>
|
||||
<g id="Clip-8"/>
|
||||
<path d="M48.9808033,106.004671 C48.6097888,106.004671 48.2317074,105.949902 47.8589262,105.835064 C19.6812588,97.2416618 -0.000176673568,69.6717516 -0.000176673568,39.142559 C-0.000176673568,26.0704817 3.71526846,13.3711856 10.7451097,2.41742443 C11.8881877,0.631254658 14.2644472,0.111834369 16.0523837,1.26021256 C17.8385535,2.40505728 18.3579738,4.78308351 17.2113623,6.56925328 C10.9783188,16.2809993 7.6833568,27.545706 7.6833568,39.142559 C7.6833568,66.3220207 25.1263382,90.870813 50.1009137,98.4854438 C52.130893,99.105568 53.273971,101.252152 52.6538468,103.282131 C52.1485604,104.937562 50.627401,106.004671 48.9808033,106.004671" id="Fill-7" mask="url(#mask-2)" [attr.fill]="color2"/>
|
||||
</g>
|
||||
<path d="M72.4972871,114.285714 C55.7249457,114.285714 40.4087071,107.985296 30.1336329,96.7687076 C28.493546,94.9775315 28.6267155,92.2090341 30.4297597,90.5795855 C32.2328039,88.953615 35.0241056,89.0840404 36.6659447,90.8752165 C45.7407417,100.778855 59.7007546,106.112386 74.9066028,105.474171 C77.350963,105.359397 79.3975672,107.253174 79.4991965,109.670393 C79.6008258,112.085872 77.7084179,114.129204 75.2710665,114.230066 C74.3441371,114.266585 73.4154554,114.285714 72.4972871,114.285714" id="Fill-10" [attr.fill]="color1"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</ng-template>
|
||||
@ -36,12 +36,10 @@
|
||||
}
|
||||
|
||||
#divider {
|
||||
width: 3px;
|
||||
width: 2px;
|
||||
height: 175px;
|
||||
left: 0;
|
||||
top: -40px;
|
||||
background-image: url('/resources/divider-new.png');
|
||||
background-repeat: repeat-y;
|
||||
position: absolute;
|
||||
img {
|
||||
position: absolute;
|
||||
|
||||
@ -2,6 +2,9 @@
|
||||
|
||||
<div class="page-title">
|
||||
<h1 i18n="shared.transaction">Transaction</h1>
|
||||
<a class="tx-link" [routerLink]="['/tx/' | relativeUrl, txId]">
|
||||
<span class="truncated">{{txId.slice(0,-4)}}</span><span class="last-four">{{txId.slice(-4)}}</span>
|
||||
</a>
|
||||
<div *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" class="features">
|
||||
<app-tx-features [tx]="tx"></app-tx-features>
|
||||
<span *ngIf="cpfpInfo && cpfpInfo.bestDescendant" class="badge badge-primary mr-1">
|
||||
@ -13,104 +16,50 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a [routerLink]="['/tx/' | relativeUrl, txId]" class="tx-link">
|
||||
{{ txId }}
|
||||
</a>
|
||||
<div class="top-data row">
|
||||
<span class="field col-sm-4 text-left">
|
||||
<ng-template [ngIf]="isLiquid && haveBlindedOutputValues(tx)" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template>
|
||||
<ng-template #defaultAmount>
|
||||
<app-amount [satoshis]="totalValue"></app-amount>
|
||||
</ng-template>
|
||||
</span>
|
||||
<span class="field col-sm-4 text-center"><ng-container *ngIf="transactionTime > 0">‎{{ transactionTime * 1000 | date:'yyyy-MM-dd HH:mm' }}</ng-container></span>
|
||||
<span class="field col-sm-4 text-right"><span class="label" i18n="transaction.fee|Transaction fee">Fee </span>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr *ngIf="tx.status.confirmed; else firstSeen">
|
||||
<td i18n="block.timestamp">Timestamp</td>
|
||||
<td>
|
||||
‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||
</td>
|
||||
</tr>
|
||||
<ng-template #firstSeen>
|
||||
<tr>
|
||||
<td i18n="transaction.first-seen|Transaction first seen">First seen</td>
|
||||
<td *ngIf="transactionTime > 0; else notSeen">
|
||||
‎{{ transactionTime * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||
</td>
|
||||
<ng-template #notSeen>
|
||||
<td>?</td>
|
||||
</ng-template>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<tr>
|
||||
<td class="td-width" i18n="dashboard.latest-transactions.amount">Amount</td>
|
||||
<td>
|
||||
<ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && haveBlindedOutputValues(tx)" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template>
|
||||
<ng-template #defaultAmount>
|
||||
<app-amount [satoshis]="getTotalTxOutput(tx)"></app-amount>
|
||||
</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.size">Size</td>
|
||||
<td [innerHTML]="'‎' + (tx.size | bytes: 2)"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.weight">Weight</td>
|
||||
<td [innerHTML]="'‎' + (tx.weight | wuBytes: 2)"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="transaction.inputs">Inputs</td>
|
||||
<td *ngIf="!isCoinbase(tx); else coinbaseInputs">{{ tx.vin.length }}</td>
|
||||
<ng-template #coinbaseInputs>
|
||||
<td i18n="transactions-list.coinbase">Coinbase</td>
|
||||
</ng-template>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="row graph-wrapper">
|
||||
<tx-bowtie-graph [tx]="tx" [width]="1112" [height]="346" [network]="network"></tx-bowtie-graph>
|
||||
<div class="above-bow">
|
||||
<p class="field pair">
|
||||
<span [innerHTML]="'‎' + (tx.size | bytes: 2)"></span>
|
||||
<span [innerHTML]="'‎' + (tx.weight | wuBytes: 2)"></span>
|
||||
</p>
|
||||
<p class="field" *ngIf="!isCoinbase(tx)">
|
||||
{{ tx.feePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||
<td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="!cpfpInfo || (!cpfpInfo.bestDescendant && !cpfpInfo.ancestors.length); else cpfpFee">
|
||||
<td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
|
||||
<td>
|
||||
{{ tx.feePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||
<ng-template [ngIf]="tx.status.confirmed">
|
||||
|
||||
<app-tx-fee-rating *ngIf="tx.fee && ((cpfpInfo && !cpfpInfo.bestDescendant && !cpfpInfo.ancestors.length) || !cpfpInfo)" [tx]="tx"></app-tx-fee-rating>
|
||||
</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
<ng-template #cpfpFee>
|
||||
<div class="overlaid">
|
||||
<ng-container [ngSwitch]="extraData">
|
||||
<table class="opreturns" *ngSwitchCase="'coinbase'">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
||||
<td>
|
||||
<div class="effective-fee-container">
|
||||
{{ tx.effectiveFeePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||
<ng-template [ngIf]="tx.status.confirmed">
|
||||
<app-tx-fee-rating class="d-none d-lg-inline ml-2" *ngIf="tx.fee" [tx]="tx"></app-tx-fee-rating>
|
||||
</ng-template>
|
||||
</div>
|
||||
</td>
|
||||
<td class="label">Coinbase</td>
|
||||
<td class="message">{{ tx.vin[0].scriptsig | hex2ascii }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<tr>
|
||||
<td i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
||||
<td [innerHTML]="'‎' + (tx.weight / 4 | vbytes: 2)"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="transaction.locktime">Locktime</td>
|
||||
<td [innerHTML]="'‎' + (tx.locktime | number)"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="transaction.outputs">Outputs</td>
|
||||
<td>{{ tx.vout.length }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="opreturns" *ngSwitchCase="'opreturn'">
|
||||
<tbody>
|
||||
<ng-container *ngFor="let vout of opReturns.slice(0,3)">
|
||||
<tr>
|
||||
<td class="label">OP_RETURN</td>
|
||||
<td *ngIf="vout.scriptpubkey_asm !== 'OP_RETURN'" class="message">{{ vout.scriptpubkey_asm | hex2ascii }}</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -10,26 +10,10 @@
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.btn-small-height {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.arrow-green {
|
||||
color: #1a9436;
|
||||
}
|
||||
|
||||
.arrow-red {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.effective-fee-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.title {
|
||||
h2 {
|
||||
line-height: 1;
|
||||
@ -46,8 +30,9 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
align-items: baseline;
|
||||
margin-bottom: 2px;
|
||||
max-width: 100%;
|
||||
|
||||
h1 {
|
||||
font-size: 52px;
|
||||
@ -58,6 +43,43 @@
|
||||
.features {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
& > * {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tx-link {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
margin: 0 1em;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
|
||||
.truncated {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: -2px;
|
||||
}
|
||||
|
||||
.last-four {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.features {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.top-data {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.table {
|
||||
@ -68,8 +90,76 @@
|
||||
}
|
||||
}
|
||||
|
||||
.field {
|
||||
font-size: 32px;
|
||||
margin: 0;
|
||||
|
||||
::ng-deep .symbol {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #ffffff66;
|
||||
}
|
||||
|
||||
&.pair > *:first-child {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.tx-link {
|
||||
display: inline-block;
|
||||
display: inline;
|
||||
font-size: 28px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.graph-wrapper {
|
||||
position: relative;
|
||||
background: #181b2d;
|
||||
padding: 10px;
|
||||
padding-bottom: 0;
|
||||
|
||||
.above-bow {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.overlaid {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-size: 28px;
|
||||
max-width: 90%;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
|
||||
.opreturns {
|
||||
width: auto;
|
||||
margin: auto;
|
||||
table-layout: auto;
|
||||
background: #2d3348af;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
|
||||
td {
|
||||
padding: 10px 10px;
|
||||
|
||||
&.message {
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,10 +7,9 @@ import {
|
||||
catchError,
|
||||
retryWhen,
|
||||
delay,
|
||||
map
|
||||
} from 'rxjs/operators';
|
||||
import { Transaction, Vout } from '../../interfaces/electrs.interface';
|
||||
import { of, merge, Subscription, Observable, Subject, timer, combineLatest, from } from 'rxjs';
|
||||
import { of, merge, Subscription, Observable, Subject, from } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { OpenGraphService } from 'src/app/services/opengraph.service';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
@ -37,6 +36,10 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
showCpfpDetails = false;
|
||||
fetchCpfp$ = new Subject<string>();
|
||||
liquidUnblinding = new LiquidUnblinding();
|
||||
isLiquid = false;
|
||||
totalValue: number;
|
||||
opReturns: Vout[];
|
||||
extraData: 'none' | 'coinbase' | 'opreturn';
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
@ -49,7 +52,12 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit() {
|
||||
this.stateService.networkChanged$.subscribe(
|
||||
(network) => (this.network = network)
|
||||
(network) => {
|
||||
this.network = network;
|
||||
if (this.network === 'liquid' || this.network == 'liquidtestnet') {
|
||||
this.isLiquid = true;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.fetchCpfpSubscription = this.fetchCpfp$
|
||||
@ -84,15 +92,16 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
txFeePerVSize: this.tx.effectiveFeePerVsize,
|
||||
});
|
||||
this.cpfpInfo = cpfpInfo;
|
||||
this.openGraphService.waitOver('cpfp-data');
|
||||
this.openGraphService.waitOver('cpfp-data-' + this.txId);
|
||||
});
|
||||
|
||||
this.subscription = this.route.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.openGraphService.waitFor('tx-data');
|
||||
const urlMatch = (params.get('id') || '').split(':');
|
||||
this.txId = urlMatch[0];
|
||||
this.openGraphService.waitFor('tx-data-' + this.txId);
|
||||
this.openGraphService.waitFor('tx-time-' + this.txId);
|
||||
this.seoService.setTitle(
|
||||
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
|
||||
);
|
||||
@ -141,7 +150,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
)
|
||||
.subscribe((tx: Transaction) => {
|
||||
if (!tx) {
|
||||
this.openGraphService.fail('tx-data');
|
||||
this.openGraphService.fail('tx-data-' + this.txId);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -152,9 +161,16 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
|
||||
this.isLoadingTx = false;
|
||||
this.error = undefined;
|
||||
this.totalValue = this.tx.vout.reduce((acc, v) => v.value + acc, 0);
|
||||
this.opReturns = this.getOpReturns(this.tx);
|
||||
this.extraData = this.chooseExtraData();
|
||||
|
||||
if (!tx.status.confirmed && tx.firstSeen) {
|
||||
if (tx.status.confirmed) {
|
||||
this.transactionTime = tx.status.block_time;
|
||||
this.openGraphService.waitOver('tx-time-' + this.txId);
|
||||
} else if (!tx.status.confirmed && tx.firstSeen) {
|
||||
this.transactionTime = tx.firstSeen;
|
||||
this.openGraphService.waitOver('tx-time-' + this.txId);
|
||||
} else {
|
||||
this.getTransactionTime();
|
||||
}
|
||||
@ -166,15 +182,15 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
bestDescendant: tx.bestDescendant,
|
||||
};
|
||||
} else {
|
||||
this.openGraphService.waitFor('cpfp-data');
|
||||
this.openGraphService.waitFor('cpfp-data-' + this.txId);
|
||||
this.fetchCpfp$.next(this.tx.txid);
|
||||
}
|
||||
}
|
||||
|
||||
this.openGraphService.waitOver('tx-data');
|
||||
this.openGraphService.waitOver('tx-data-' + this.txId);
|
||||
},
|
||||
(error) => {
|
||||
this.openGraphService.fail('tx-data');
|
||||
this.openGraphService.fail('tx-data-' + this.txId);
|
||||
this.error = error;
|
||||
this.isLoadingTx = false;
|
||||
}
|
||||
@ -182,7 +198,6 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
getTransactionTime() {
|
||||
this.openGraphService.waitFor('tx-time');
|
||||
this.apiService
|
||||
.getTransactionTimes$([this.tx.txid])
|
||||
.pipe(
|
||||
@ -192,7 +207,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
)
|
||||
.subscribe((transactionTimes) => {
|
||||
this.transactionTime = transactionTimes[0];
|
||||
this.openGraphService.waitOver('tx-time');
|
||||
this.openGraphService.waitOver('tx-time-' + this.txId);
|
||||
});
|
||||
}
|
||||
|
||||
@ -217,6 +232,20 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
return tx.vout.map((v: Vout) => v.value || 0).reduce((a: number, b: number) => a + b);
|
||||
}
|
||||
|
||||
getOpReturns(tx: Transaction): Vout[] {
|
||||
return tx.vout.filter((v) => v.scriptpubkey_type === 'op_return' && v.scriptpubkey_asm !== 'OP_RETURN');
|
||||
}
|
||||
|
||||
chooseExtraData(): 'none' | 'opreturn' | 'coinbase' {
|
||||
if (this.isCoinbase(this.tx)) {
|
||||
return 'coinbase';
|
||||
} else if (this.opReturns?.length) {
|
||||
return 'opreturn';
|
||||
} else {
|
||||
return 'none';
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscription.unsubscribe();
|
||||
this.fetchCpfpSubscription.unsubscribe();
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
<div class="col">
|
||||
<table class="table table-borderless smaller-text table-sm table-tx-vin">
|
||||
<tbody>
|
||||
<ng-template ngFor let-vin [ngForOf]="tx['@vinLimit'] ? ((tx.vin.length > rowLimit) ? tx.vin.slice(0, rowLimit - 2) : tx.vin.slice(0, rowLimit)) : tx.vin" [ngForTrackBy]="trackByIndexFn">
|
||||
<ng-template ngFor let-vin let-vindex="index" [ngForOf]="tx['@vinLimit'] ? ((tx.vin.length > rowLimit) ? tx.vin.slice(0, rowLimit - 2) : tx.vin.slice(0, rowLimit)) : tx.vin" [ngForTrackBy]="trackByIndexFn">
|
||||
<tr [ngClass]="{
|
||||
'assetBox': assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded,
|
||||
'highlight': vin.prevout?.scriptpubkey_address === this.address && this.address !== ''
|
||||
@ -77,7 +77,7 @@
|
||||
{{ vin.prevout.scriptpubkey_type?.toUpperCase() }}
|
||||
</ng-template>
|
||||
<div>
|
||||
<app-address-labels [vin]="vin" [channel]="channels && channels.inputs[i] || null"></app-address-labels>
|
||||
<app-address-labels [vin]="vin" [channel]="tx._channels && tx._channels.inputs[vindex] || null"></app-address-labels>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
@ -172,7 +172,7 @@
|
||||
</span>
|
||||
</a>
|
||||
<div>
|
||||
<app-address-labels [vout]="vout" [channel]="channels && channels.outputs[i] && channels.outputs[i].transaction_vout === vindex ? channels.outputs[i] : null"></app-address-labels>
|
||||
<app-address-labels [vout]="vout" [channel]="tx._channels && tx._channels.outputs[vindex] ? tx._channels.outputs[vindex] : null"></app-address-labels>
|
||||
</div>
|
||||
<ng-template #scriptpubkey_type>
|
||||
<ng-template [ngIf]="vout.pegout" [ngIfElse]="defaultscriptpubkey_type">
|
||||
@ -212,15 +212,15 @@
|
||||
</ng-template>
|
||||
</td>
|
||||
<td class="arrow-td">
|
||||
<span *ngIf="!outspends[i] || vout.scriptpubkey_type === 'op_return' || vout.scriptpubkey_type === 'fee' ; else outspend" class="grey">
|
||||
<span *ngIf="!tx._outspends || vout.scriptpubkey_type === 'op_return' || vout.scriptpubkey_type === 'fee' ; else outspend" class="grey">
|
||||
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
||||
</span>
|
||||
<ng-template #outspend>
|
||||
<span *ngIf="!outspends[i][vindex] || !outspends[i][vindex].spent; else spent" class="green">
|
||||
<span *ngIf="!tx._outspends[vindex] || !tx._outspends[vindex].spent; else spent" class="green">
|
||||
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
||||
</span>
|
||||
<ng-template #spent>
|
||||
<a *ngIf="outspends[i][vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, outspends[i][vindex].txid]" class="red">
|
||||
<a *ngIf="tx._outspends[vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, tx._outspends[vindex].txid]" class="red">
|
||||
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
||||
</a>
|
||||
<ng-template #outputNoTxId>
|
||||
|
||||
@ -27,7 +27,6 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
@Input() outputIndex: number;
|
||||
@Input() address: string = '';
|
||||
@Input() rowLimit = 12;
|
||||
@Input() channels: { inputs: any[], outputs: any[] };
|
||||
|
||||
@Output() loadMore = new EventEmitter();
|
||||
|
||||
@ -36,8 +35,8 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
refreshOutspends$: ReplaySubject<string[]> = new ReplaySubject();
|
||||
refreshChannels$: ReplaySubject<string[]> = new ReplaySubject();
|
||||
showDetails$ = new BehaviorSubject<boolean>(false);
|
||||
outspends: Outspend[][] = [];
|
||||
assetsMinimal: any;
|
||||
transactionsLength: number = 0;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
@ -47,7 +46,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
private ref: ChangeDetectorRef,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
ngOnInit(): void {
|
||||
this.latestBlock$ = this.stateService.blocks$.pipe(map(([block]) => block));
|
||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||
|
||||
@ -62,14 +61,20 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
.pipe(
|
||||
switchMap((txIds) => this.apiService.getOutspendsBatched$(txIds)),
|
||||
tap((outspends: Outspend[][]) => {
|
||||
this.outspends = this.outspends.concat(outspends);
|
||||
if (!this.transactions) {
|
||||
return;
|
||||
}
|
||||
const transactions = this.transactions.filter((tx) => !tx._outspends);
|
||||
outspends.forEach((outspend, i) => {
|
||||
transactions[i]._outspends = outspend;
|
||||
});
|
||||
}),
|
||||
),
|
||||
this.stateService.utxoSpent$
|
||||
.pipe(
|
||||
tap((utxoSpent) => {
|
||||
for (const i in utxoSpent) {
|
||||
this.outspends[0][i] = {
|
||||
this.transactions[0]._outspends[i] = {
|
||||
spent: true,
|
||||
txid: utxoSpent[i].txid,
|
||||
vin: utxoSpent[i].vin,
|
||||
@ -81,21 +86,23 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
.pipe(
|
||||
filter(() => this.stateService.env.LIGHTNING),
|
||||
switchMap((txIds) => this.apiService.getChannelByTxIds$(txIds)),
|
||||
map((channels) => {
|
||||
this.channels = channels;
|
||||
tap((channels) => {
|
||||
const transactions = this.transactions.filter((tx) => !tx._channels);
|
||||
channels.forEach((channel, i) => {
|
||||
transactions[i]._channels = channel;
|
||||
});
|
||||
}),
|
||||
)
|
||||
,
|
||||
).subscribe(() => this.ref.markForCheck());
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
ngOnChanges(): void {
|
||||
if (!this.transactions || !this.transactions.length) {
|
||||
return;
|
||||
}
|
||||
if (this.paginated) {
|
||||
this.outspends = [];
|
||||
}
|
||||
|
||||
this.transactionsLength = this.transactions.length;
|
||||
if (this.outputIndex) {
|
||||
setTimeout(() => {
|
||||
const assetBoxElements = document.getElementsByClassName('assetBox');
|
||||
@ -105,10 +112,10 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
}, 10);
|
||||
}
|
||||
|
||||
this.transactions.forEach((tx, i) => {
|
||||
this.transactions.forEach((tx) => {
|
||||
tx['@voutLimit'] = true;
|
||||
tx['@vinLimit'] = true;
|
||||
if (this.outspends[i]) {
|
||||
if (tx['addressValue'] !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -126,14 +133,19 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
tx['addressValue'] = addressIn - addressOut;
|
||||
}
|
||||
});
|
||||
const txIds = this.transactions.map((tx) => tx.txid);
|
||||
this.refreshOutspends$.next(txIds);
|
||||
if (!this.channels) {
|
||||
this.refreshChannels$.next(txIds);
|
||||
const txIds = this.transactions.filter((tx) => !tx._outspends).map((tx) => tx.txid);
|
||||
if (txIds.length) {
|
||||
this.refreshOutspends$.next(txIds);
|
||||
}
|
||||
if (this.stateService.env.LIGHTNING) {
|
||||
const txIds = this.transactions.filter((tx) => !tx._channels).map((tx) => tx.txid);
|
||||
if (txIds.length) {
|
||||
this.refreshChannels$.next(txIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onScroll() {
|
||||
onScroll(): void {
|
||||
const scrollHeight = document.body.scrollHeight;
|
||||
const scrollTop = document.documentElement.scrollTop;
|
||||
if (scrollHeight > 0){
|
||||
@ -148,11 +160,11 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
return tx.vout.some((v: any) => v.value === undefined);
|
||||
}
|
||||
|
||||
getTotalTxOutput(tx: Transaction) {
|
||||
getTotalTxOutput(tx: Transaction): number {
|
||||
return tx.vout.map((v: Vout) => v.value || 0).reduce((a: number, b: number) => a + b);
|
||||
}
|
||||
|
||||
switchCurrency() {
|
||||
switchCurrency(): void {
|
||||
if (this.network === 'liquid' || this.network === 'liquidtestnet') {
|
||||
return;
|
||||
}
|
||||
@ -164,7 +176,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
return tx.txid + tx.status.confirmed;
|
||||
}
|
||||
|
||||
trackByIndexFn(index: number) {
|
||||
trackByIndexFn(index: number): number {
|
||||
return index;
|
||||
}
|
||||
|
||||
@ -177,7 +189,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
return Math.pow(base, exponent);
|
||||
}
|
||||
|
||||
toggleDetails() {
|
||||
toggleDetails(): void {
|
||||
if (this.showDetails$.value === true) {
|
||||
this.showDetails$.next(false);
|
||||
} else {
|
||||
@ -185,7 +197,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
loadMoreInputs(tx: Transaction) {
|
||||
loadMoreInputs(tx: Transaction): void {
|
||||
tx['@vinLimit'] = false;
|
||||
|
||||
this.electrsApiService.getTransaction$(tx.txid)
|
||||
@ -196,7 +208,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
ngOnDestroy(): void {
|
||||
this.outspendsSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
<svg *ngIf="inputs && outputs" class="bowtie" [attr.height]="(height + 10) + 'px'" [attr.width]="width + 'px'">
|
||||
<defs>
|
||||
<marker id="input-arrow" viewBox="-5 -5 10 10"
|
||||
refX="0" refY="0"
|
||||
markerUnits="strokeWidth"
|
||||
markerWidth="1.5" markerHeight="1"
|
||||
orient="auto">
|
||||
<path d="M -5 -5 L 0 0 L -5 5 L 1 5 L 1 -5 Z" stroke-width="0" [attr.fill]="gradient[0]"/>
|
||||
</marker>
|
||||
<marker id="output-arrow" viewBox="-5 -5 10 10"
|
||||
refX="0" refY="0"
|
||||
markerUnits="strokeWidth"
|
||||
markerWidth="1.5" markerHeight="1"
|
||||
orient="auto">
|
||||
<path d="M 1 -5 L 0 -5 L -5 0 L 0 5 L 1 5 Z" stroke-width="0" [attr.fill]="gradient[0]"/>
|
||||
</marker>
|
||||
<marker id="fee-arrow" viewBox="-5 -5 10 10"
|
||||
refX="0" refY="0"
|
||||
markerUnits="strokeWidth"
|
||||
markerWidth="1.5" markerHeight="1"
|
||||
orient="auto">
|
||||
</marker>
|
||||
<linearGradient id="input-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" [attr.stop-color]="gradient[0]" />
|
||||
<stop offset="100%" [attr.stop-color]="gradient[1]" />
|
||||
</linearGradient>
|
||||
<linearGradient id="output-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" [attr.stop-color]="gradient[1]" />
|
||||
<stop offset="100%" [attr.stop-color]="gradient[0]" />
|
||||
</linearGradient>
|
||||
<linearGradient id="fee-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" [attr.stop-color]="gradient[1]" />
|
||||
<stop offset="50%" [attr.stop-color]="gradient[1]" />
|
||||
<stop offset="100%" stop-color="transparent" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path [attr.d]="middle.path" class="line middle" [style]="middle.style"/>
|
||||
<ng-container *ngFor="let input of inputs">
|
||||
<path [attr.d]="input.path" class="line {{input.class}}" [style]="input.style" attr.marker-start="url(#{{input.class}}-arrow)"/>
|
||||
</ng-container>
|
||||
<ng-container *ngFor="let output of outputs">
|
||||
<path [attr.d]="output.path" class="line {{output.class}}" [style]="output.style" attr.marker-start="url(#{{output.class}}-arrow)" />
|
||||
</ng-container>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@ -0,0 +1,15 @@
|
||||
.bowtie {
|
||||
.line {
|
||||
fill: none;
|
||||
|
||||
&.input {
|
||||
stroke: url(#input-gradient);
|
||||
}
|
||||
&.output {
|
||||
stroke: url(#output-gradient);
|
||||
}
|
||||
&.fee {
|
||||
stroke: url(#fee-gradient);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,169 @@
|
||||
import { Component, OnInit, Input, OnChanges } from '@angular/core';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
|
||||
interface SvgLine {
|
||||
path: string;
|
||||
style: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'tx-bowtie-graph',
|
||||
templateUrl: './tx-bowtie-graph.component.html',
|
||||
styleUrls: ['./tx-bowtie-graph.component.scss'],
|
||||
})
|
||||
export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
@Input() tx: Transaction;
|
||||
@Input() network: string;
|
||||
@Input() width = 1200;
|
||||
@Input() height = 600;
|
||||
@Input() combinedWeight = 100;
|
||||
@Input() minWeight = 2; //
|
||||
@Input() maxStrands = 24; // number of inputs/outputs to keep fully on-screen.
|
||||
|
||||
inputs: SvgLine[];
|
||||
outputs: SvgLine[];
|
||||
middle: SvgLine;
|
||||
isLiquid: boolean = false;
|
||||
|
||||
gradientColors = {
|
||||
'': ['#9339f4', '#105fb0'],
|
||||
bisq: ['#9339f4', '#105fb0'],
|
||||
// liquid: ['#116761', '#183550'],
|
||||
liquid: ['#09a197', '#0f62af'],
|
||||
// 'liquidtestnet': ['#494a4a', '#272e46'],
|
||||
'liquidtestnet': ['#d2d2d2', '#979797'],
|
||||
// testnet: ['#1d486f', '#183550'],
|
||||
testnet: ['#4edf77', '#10a0af'],
|
||||
// signet: ['#6f1d5d', '#471850'],
|
||||
signet: ['#d24fc8', '#a84fd2'],
|
||||
};
|
||||
|
||||
gradient: string[] = ['#105fb0', '#105fb0'];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet');
|
||||
this.gradient = this.gradientColors[this.network];
|
||||
this.initGraph();
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet');
|
||||
this.gradient = this.gradientColors[this.network];
|
||||
this.initGraph();
|
||||
}
|
||||
|
||||
initGraph(): void {
|
||||
const totalValue = this.calcTotalValue(this.tx);
|
||||
const voutWithFee = this.tx.vout.map(v => { return { type: v.scriptpubkey_type === 'fee' ? 'fee' : 'output', value: v?.value }; });
|
||||
|
||||
if (this.tx.fee && !this.isLiquid) {
|
||||
voutWithFee.unshift({ type: 'fee', value: this.tx.fee });
|
||||
}
|
||||
|
||||
this.inputs = this.initLines('in', this.tx.vin.map(v => { return {type: 'input', value: v?.prevout?.value }; }), totalValue, this.maxStrands);
|
||||
this.outputs = this.initLines('out', voutWithFee, totalValue, this.maxStrands);
|
||||
|
||||
this.middle = {
|
||||
path: `M ${(this.width / 2) - 50} ${(this.height / 2) + 0.5} L ${(this.width / 2) + 50} ${(this.height / 2) + 0.5}`,
|
||||
style: `stroke-width: ${this.combinedWeight + 0.5}; stroke: ${this.gradient[1]}`
|
||||
};
|
||||
}
|
||||
|
||||
calcTotalValue(tx: Transaction): number {
|
||||
const totalOutput = this.tx.vout.reduce((acc, v) => (v.value == null ? 0 : v.value) + acc, 0);
|
||||
// simple sum of outputs + fee for bitcoin
|
||||
if (!this.isLiquid) {
|
||||
return this.tx.fee ? totalOutput + this.tx.fee : totalOutput;
|
||||
} else {
|
||||
const totalInput = this.tx.vin.reduce((acc, v) => (v?.prevout?.value == null ? 0 : v.prevout.value) + acc, 0);
|
||||
const confidentialInputCount = this.tx.vin.reduce((acc, v) => acc + (v?.prevout?.value == null ? 1 : 0), 0);
|
||||
const confidentialOutputCount = this.tx.vout.reduce((acc, v) => acc + (v.value == null ? 1 : 0), 0);
|
||||
|
||||
// if there are unknowns on both sides, the total is indeterminate, so we'll just fudge it
|
||||
if (confidentialInputCount && confidentialOutputCount) {
|
||||
const knownInputCount = (tx.vin.length - confidentialInputCount) || 1;
|
||||
const knownOutputCount = (tx.vout.length - confidentialOutputCount) || 1;
|
||||
// assume confidential inputs/outputs have the same average value as the known ones
|
||||
const adjustedTotalInput = totalInput + ((totalInput / knownInputCount) * confidentialInputCount);
|
||||
const adjustedTotalOutput = totalOutput + ((totalOutput / knownOutputCount) * confidentialOutputCount);
|
||||
return Math.max(adjustedTotalInput, adjustedTotalOutput) || 1;
|
||||
} else {
|
||||
// otherwise knowing the actual total of one side suffices
|
||||
return Math.max(totalInput, totalOutput) || 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initLines(side: 'in' | 'out', xputs: { type: string, value: number | void }[], total: number, maxVisibleStrands: number): SvgLine[] {
|
||||
const lines = [];
|
||||
let unknownCount = 0;
|
||||
let unknownTotal = total == null ? this.combinedWeight : total;
|
||||
xputs.forEach(put => {
|
||||
if (put.value == null) {
|
||||
unknownCount++;
|
||||
} else {
|
||||
unknownTotal -= put.value as number;
|
||||
}
|
||||
});
|
||||
const unknownShare = unknownTotal / unknownCount;
|
||||
|
||||
// conceptual weights
|
||||
const weights = xputs.map((put): number => this.combinedWeight * (put.value == null ? unknownShare : put.value as number) / total);
|
||||
// actual displayed line thicknesses
|
||||
const minWeights = weights.map((w) => Math.max(this.minWeight - 1, w) + 1);
|
||||
const visibleStrands = Math.min(maxVisibleStrands, xputs.length);
|
||||
const visibleWeight = minWeights.slice(0, visibleStrands).reduce((acc, v) => v + acc, 0);
|
||||
const gaps = visibleStrands - 1;
|
||||
|
||||
const innerTop = (this.height / 2) - (this.combinedWeight / 2);
|
||||
const innerBottom = innerTop + this.combinedWeight;
|
||||
// tracks the visual bottom of the endpoints of the previous line
|
||||
let lastOuter = 0;
|
||||
let lastInner = innerTop;
|
||||
// gap between strands
|
||||
const spacing = (this.height - visibleWeight) / gaps;
|
||||
|
||||
for (let i = 0; i < xputs.length; i++) {
|
||||
const weight = weights[i];
|
||||
const minWeight = minWeights[i];
|
||||
// set the vertical position of the (center of the) outer side of the line
|
||||
let outer = lastOuter + (minWeight / 2);
|
||||
const inner = Math.min(innerBottom + (minWeight / 2), Math.max(innerTop + (minWeight / 2), lastInner + (weight / 2)));
|
||||
|
||||
// special case to center single input/outputs
|
||||
if (xputs.length === 1) {
|
||||
outer = (this.height / 2);
|
||||
}
|
||||
|
||||
lastOuter += minWeight + spacing;
|
||||
lastInner += weight;
|
||||
lines.push({
|
||||
path: this.makePath(side, outer, inner, minWeight),
|
||||
style: this.makeStyle(minWeight, xputs[i].type),
|
||||
class: xputs[i].type
|
||||
});
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
makePath(side: 'in' | 'out', outer: number, inner: number, weight: number): string {
|
||||
const start = side === 'in' ? (weight * 0.5) : this.width - (weight * 0.5);
|
||||
const center = this.width / 2 + (side === 'in' ? -45 : 45 );
|
||||
const midpoint = (start + center) / 2;
|
||||
// correct for svg horizontal gradient bug
|
||||
if (Math.round(outer) === Math.round(inner)) {
|
||||
outer -= 1;
|
||||
}
|
||||
return `M ${start} ${outer} C ${midpoint} ${outer}, ${midpoint} ${inner}, ${center} ${inner}`;
|
||||
}
|
||||
|
||||
makeStyle(minWeight, type): string {
|
||||
if (type === 'fee') {
|
||||
return `stroke-width: ${minWeight}; stroke: url(#fee-gradient)`;
|
||||
} else {
|
||||
return `stroke-width: ${minWeight}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -114,11 +114,14 @@ export const restApiDocsData = [
|
||||
curl: [],
|
||||
response: `{
|
||||
progressPercent: 44.397234501112074,
|
||||
difficultyChange: 0.9845932018381687,
|
||||
estimatedRetargetDate: 1627762478.9111245,
|
||||
difficultyChange: 98.45932018381687,
|
||||
estimatedRetargetDate: 1627762478,
|
||||
remainingBlocks: 1121,
|
||||
remainingTime: 665977.6261244365,
|
||||
previousRetarget: -4.807005268478962
|
||||
remainingTime: 665977,
|
||||
previousRetarget: -4.807005268478962,
|
||||
nextRetargetHeight: 741888,
|
||||
timeAvg: 302328,
|
||||
timeOffset: 0
|
||||
}`
|
||||
},
|
||||
codeSampleTestnet: {
|
||||
@ -127,11 +130,14 @@ export const restApiDocsData = [
|
||||
curl: [],
|
||||
response: `{
|
||||
progressPercent: 44.397234501112074,
|
||||
difficultyChange: 0.9845932018381687,
|
||||
estimatedRetargetDate: 1627762478.9111245,
|
||||
difficultyChange: 98.45932018381687,
|
||||
estimatedRetargetDate: 1627762478,
|
||||
remainingBlocks: 1121,
|
||||
remainingTime: 665977.6261244365,
|
||||
previousRetarget: -4.807005268478962
|
||||
remainingTime: 665977,
|
||||
previousRetarget: -4.807005268478962,
|
||||
nextRetargetHeight: 741888,
|
||||
timeAvg: 302328,
|
||||
timeOffset: 0
|
||||
}`
|
||||
},
|
||||
codeSampleSignet: {
|
||||
@ -140,11 +146,14 @@ export const restApiDocsData = [
|
||||
curl: [],
|
||||
response: `{
|
||||
progressPercent: 44.397234501112074,
|
||||
difficultyChange: 0.9845932018381687,
|
||||
estimatedRetargetDate: 1627762478.9111245,
|
||||
difficultyChange: 98.45932018381687,
|
||||
estimatedRetargetDate: 1627762478,
|
||||
remainingBlocks: 1121,
|
||||
remainingTime: 665977.6261244365,
|
||||
previousRetarget: -4.807005268478962
|
||||
remainingTime: 665977,
|
||||
previousRetarget: -4.807005268478962,
|
||||
nextRetargetHeight: 741888,
|
||||
timeAvg: 302328,
|
||||
timeOffset: 0
|
||||
}`
|
||||
},
|
||||
codeSampleLiquid: {
|
||||
@ -153,11 +162,14 @@ export const restApiDocsData = [
|
||||
curl: [],
|
||||
response: `{
|
||||
progressPercent: 44.397234501112074,
|
||||
difficultyChange: 0.9845932018381687,
|
||||
estimatedRetargetDate: 1627762478.9111245,
|
||||
difficultyChange: 98.45932018381687,
|
||||
estimatedRetargetDate: 1627762478,
|
||||
remainingBlocks: 1121,
|
||||
remainingTime: 665977.6261244365,
|
||||
previousRetarget: -4.807005268478962
|
||||
remainingTime: 665977,
|
||||
previousRetarget: -4.807005268478962,
|
||||
nextRetargetHeight: 741888,
|
||||
timeAvg: 302328,
|
||||
timeOffset: 0
|
||||
}`
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { IChannel } from './node-api.interface';
|
||||
|
||||
export interface Transaction {
|
||||
txid: string;
|
||||
version: number;
|
||||
@ -19,6 +21,13 @@ export interface Transaction {
|
||||
deleteAfter?: number;
|
||||
_unblinded?: any;
|
||||
_deduced?: boolean;
|
||||
_outspends?: Outspend[];
|
||||
_channels?: TransactionChannels;
|
||||
}
|
||||
|
||||
export interface TransactionChannels {
|
||||
inputs: { [vin: number]: IChannel };
|
||||
outputs: { [vout: number]: IChannel };
|
||||
}
|
||||
|
||||
interface Ancestor {
|
||||
|
||||
@ -188,4 +188,36 @@ export interface IOldestNodes {
|
||||
updatedAt?: number,
|
||||
city?: any,
|
||||
country?: any,
|
||||
}
|
||||
}
|
||||
|
||||
export interface IChannel {
|
||||
id: number;
|
||||
short_id: string;
|
||||
capacity: number;
|
||||
transaction_id: string;
|
||||
transaction_vout: number;
|
||||
closing_transaction_id: string;
|
||||
closing_reason: string;
|
||||
updated_at: string;
|
||||
created: string;
|
||||
status: number;
|
||||
node_left: Node,
|
||||
node_right: Node,
|
||||
}
|
||||
|
||||
|
||||
export interface INode {
|
||||
alias: string;
|
||||
public_key: string;
|
||||
channels: number;
|
||||
capacity: number;
|
||||
base_fee_mtokens: number;
|
||||
cltv_delta: number;
|
||||
fee_rate: number;
|
||||
is_disabled: boolean;
|
||||
max_htlc_mtokens: number;
|
||||
min_htlc_mtokens: number;
|
||||
updated_at: string;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
<div class="mb-2 box-top">
|
||||
<div class="box-left">
|
||||
<h3 class="mb-0">{{ channel.alias || '?' }}</h3>
|
||||
<div class="box-left text-truncate">
|
||||
<h3 class="mb-0 text-truncate">{{ channel.alias || '?' }}</h3>
|
||||
<a [routerLink]="['/lightning/node' | relativeUrl, channel.public_key]" >
|
||||
{{ channel.public_key | shortenString : 12 }}
|
||||
</a>
|
||||
<app-clipboard [text]="channel.node1_public_key"></app-clipboard>
|
||||
<app-clipboard [text]="channel.public_key"></app-clipboard>
|
||||
</div>
|
||||
<div class="box-right">
|
||||
<div class="second-line">{{ channel.channels }} channels</div>
|
||||
@ -51,4 +51,4 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||
|
||||
@ -8,8 +8,8 @@
|
||||
|
||||
.box-right {
|
||||
text-align: right;
|
||||
width: 50%;
|
||||
margin-top: auto;
|
||||
margin: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.shared-block {
|
||||
|
||||
@ -58,7 +58,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md map-col">
|
||||
<app-nodes-channels-map *ngIf="!error" [style]="'channelpage'" [channel]="channelGeo" [fitContainer]="true" (readyEvent)="onMapReady()"></app-nodes-channels-map>
|
||||
<app-nodes-channels-map *ngIf="!error" [style]="'channelpage'" [channel]="channelGeo" [fitContainer]="true" [placeholder]="true" (readyEvent)="onMapReady()"></app-nodes-channels-map>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -16,6 +16,7 @@ export class ChannelPreviewComponent implements OnInit {
|
||||
channel$: Observable<any>;
|
||||
error: any = null;
|
||||
channelGeo: number[] = [];
|
||||
shortId: string;
|
||||
|
||||
constructor(
|
||||
private lightningApiService: LightningApiService,
|
||||
@ -28,8 +29,9 @@ export class ChannelPreviewComponent implements OnInit {
|
||||
this.channel$ = this.activatedRoute.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.openGraphService.waitFor('channel-map');
|
||||
this.openGraphService.waitFor('channel-data');
|
||||
this.shortId = params.get('short_id') || '';
|
||||
this.openGraphService.waitFor('channel-map-' + this.shortId);
|
||||
this.openGraphService.waitFor('channel-data-' + this.shortId);
|
||||
this.error = null;
|
||||
this.seoService.setTitle(`Channel: ${params.get('short_id')}`);
|
||||
return this.lightningApiService.getChannel$(params.get('short_id'))
|
||||
@ -48,12 +50,12 @@ export class ChannelPreviewComponent implements OnInit {
|
||||
data.node_right.longitude, data.node_right.latitude,
|
||||
];
|
||||
}
|
||||
this.openGraphService.waitOver('channel-data');
|
||||
this.openGraphService.waitOver('channel-data-' + this.shortId);
|
||||
}),
|
||||
catchError((err) => {
|
||||
this.error = err;
|
||||
this.openGraphService.fail('channel-map');
|
||||
this.openGraphService.fail('channel-data');
|
||||
this.openGraphService.fail('channel-map-' + this.shortId);
|
||||
this.openGraphService.fail('channel-data-' + this.shortId);
|
||||
return of(null);
|
||||
})
|
||||
);
|
||||
@ -62,6 +64,6 @@ export class ChannelPreviewComponent implements OnInit {
|
||||
}
|
||||
|
||||
onMapReady() {
|
||||
this.openGraphService.waitOver('channel-map');
|
||||
this.openGraphService.waitOver('channel-map-' + this.shortId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div class="container-xl" *ngIf="(channel$ | async) as channel">
|
||||
<div class="container-xl" *ngIf="(channel$ | async) as channel; else skeletonLoader">
|
||||
<h5 class="mb-0" style="color: #ffffff66" i18n="lightning.channel">Lightning channel</h5>
|
||||
<div class="title-container">
|
||||
<h1 class="mb-0">{{ channel.short_id }}</h1>
|
||||
@ -11,6 +11,7 @@
|
||||
<span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0">Inactive</span>
|
||||
<span class="badge rounded-pill badge-success" *ngIf="channel.status === 1">Active</span>
|
||||
<span class="badge rounded-pill badge-danger" *ngIf="channel.status === 2">Closed</span>
|
||||
<app-closing-type *ngIf="channel.closing_reason" [type]="channel.closing_reason"></app-closing-type>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
@ -64,14 +65,18 @@
|
||||
|
||||
<ng-container *ngIf="transactions$ | async as transactions">
|
||||
<ng-template [ngIf]="transactions[0]">
|
||||
<h3>Opening transaction</h3>
|
||||
<app-transactions-list [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5" [channels]="{ inputs: [], outputs: [channel] }"></app-transactions-list>
|
||||
<div class="d-flex">
|
||||
<h3>Opening transaction</h3>
|
||||
<button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList1.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button>
|
||||
</div>
|
||||
<app-transactions-list #txList1 [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="transactions[1]">
|
||||
<div class="closing-header">
|
||||
<div class="closing-header d-flex">
|
||||
<h3 style="margin: 0;">Closing transaction</h3> <app-closing-type [type]="channel.closing_reason"></app-closing-type>
|
||||
<button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList2.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button>
|
||||
</div>
|
||||
<app-transactions-list [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5" [channels]="{ inputs: [channel], outputs: [] }"></app-transactions-list>
|
||||
<app-transactions-list #txList2 [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
@ -85,4 +90,62 @@
|
||||
<br><br>
|
||||
<i>{{ error.status }}: {{ error.error }}</i>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #skeletonLoader>
|
||||
<div class="container-xl">
|
||||
<h5 class="mb-0" style="color: #ffffff66" i18n="lightning.channel">Lightning channel</h5>
|
||||
<div class="title-container">
|
||||
<h1 class="mb-0"><span class="skeleton-loader" style="width: 275px; height: 25px;"></span></h1>
|
||||
<span class="tx-link">
|
||||
<span class="skeleton-loader" style="margin-bottom: 5px; width: 210px;"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="badges mb-2">
|
||||
<span class="skeleton-loader" style="width: 50px; height: 22px; margin-top: 5px;"></span>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div style="height: 413px; padding: 15px;">
|
||||
<div class="text-center loading-spinner">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="box">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<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>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="w-100 d-block d-md-none"></div>
|
||||
<div class="col-md">
|
||||
<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>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ng-template>
|
||||
@ -30,6 +30,10 @@
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
app-fiat {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
@ -41,14 +45,41 @@ app-fiat {
|
||||
}
|
||||
|
||||
.closing-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 0rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
h3 {
|
||||
font-size: 1.4rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.closing-header {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
app-closing-type {
|
||||
flex-basis: 100%;
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
position: absolute;
|
||||
top: 400px;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
@media (max-width: 767.98px) {
|
||||
top: 450px;
|
||||
}
|
||||
}
|
||||
|
||||
.details-button {
|
||||
align-self: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { forkJoin, Observable, of, share, zip } from 'rxjs';
|
||||
import { catchError, map, shareReplay, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { Observable, of, zip } from 'rxjs';
|
||||
import { catchError, map, shareReplay, switchMap } from 'rxjs/operators';
|
||||
import { IChannel } from 'src/app/interfaces/node-api.interface';
|
||||
import { ElectrsApiService } from 'src/app/services/electrs-api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
@ -62,10 +62,15 @@ export class ChannelComponent implements OnInit {
|
||||
);
|
||||
|
||||
this.transactions$ = this.channel$.pipe(
|
||||
switchMap((data) => {
|
||||
switchMap((channel: IChannel) => {
|
||||
return zip([
|
||||
data.transaction_id ? this.electrsApiService.getTransaction$(data.transaction_id) : of(null),
|
||||
data.closing_transaction_id ? this.electrsApiService.getTransaction$(data.closing_transaction_id) : of(null),
|
||||
channel.transaction_id ? this.electrsApiService.getTransaction$(channel.transaction_id) : of(null),
|
||||
channel.closing_transaction_id ? this.electrsApiService.getTransaction$(channel.closing_transaction_id).pipe(
|
||||
map((tx) => {
|
||||
tx._channels = { inputs: {0: channel}, outputs: {}};
|
||||
return tx;
|
||||
})
|
||||
) : of(null),
|
||||
]);
|
||||
}),
|
||||
);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<div *ngIf="channels$ | async as response; else skeleton">
|
||||
<form [formGroup]="channelStatusForm" class="formRadioGroup float-right">
|
||||
<div *ngIf="channels$ | async as response; else skeleton" style="position: relative;">
|
||||
<form [formGroup]="channelStatusForm" class="formRadioGroup">
|
||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="status">
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'open'" fragment="open" i18n="open">Open
|
||||
@ -10,7 +10,7 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<table class="table table-borderless" *ngIf="response.channels.length > 0">
|
||||
<table class="table table-borderless" *ngIf="response.channels.length > 0" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
<ng-container *ngTemplateOutlet="tableHeader"></ng-container>
|
||||
<tbody>
|
||||
<tr *ngFor="let channel of response.channels; let i = index;">
|
||||
@ -87,8 +87,6 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #skeleton>
|
||||
<h2 class="float-left">Channels</h2>
|
||||
|
||||
<table class="table table-borderless">
|
||||
<ng-container *ngTemplateOutlet="tableHeader"></ng-container>
|
||||
<tbody>
|
||||
|
||||
@ -7,3 +7,20 @@
|
||||
font-size: 12px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.formRadioGroup {
|
||||
@media (min-width: 435px) {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -46px;
|
||||
}
|
||||
@media (max-width: 435px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
@media (max-width: 435px) {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,7 @@ import { LightningApiService } from '../lightning-api.service';
|
||||
export class ChannelsListComponent implements OnInit, OnChanges {
|
||||
@Input() publicKey: string;
|
||||
@Output() channelsStatusChangedEvent = new EventEmitter<string>();
|
||||
@Output() loadingEvent = new EventEmitter<boolean>(false);
|
||||
channels$: Observable<any>;
|
||||
|
||||
// @ts-ignore
|
||||
@ -26,6 +27,7 @@ export class ChannelsListComponent implements OnInit, OnChanges {
|
||||
defaultStatus = 'open';
|
||||
status = 'open';
|
||||
publicKeySize = 25;
|
||||
isLoading = false;
|
||||
|
||||
constructor(
|
||||
private lightningApiService: LightningApiService,
|
||||
@ -56,6 +58,8 @@ export class ChannelsListComponent implements OnInit, OnChanges {
|
||||
)
|
||||
.pipe(
|
||||
tap((val) => {
|
||||
this.isLoading = true;
|
||||
this.loadingEvent.emit(true);
|
||||
if (typeof val === 'string') {
|
||||
this.status = val;
|
||||
this.page = 1;
|
||||
@ -64,10 +68,12 @@ export class ChannelsListComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}),
|
||||
switchMap(() => {
|
||||
this.channelsStatusChangedEvent.emit(this.status);
|
||||
return this.lightningApiService.getChannelsByNodeId$(this.publicKey, (this.page - 1) * this.itemsPerPage, this.status);
|
||||
this.channelsStatusChangedEvent.emit(this.status);
|
||||
return this.lightningApiService.getChannelsByNodeId$(this.publicKey, (this.page - 1) * this.itemsPerPage, this.status);
|
||||
}),
|
||||
map((response) => {
|
||||
this.isLoading = false;
|
||||
this.loadingEvent.emit(false);
|
||||
return {
|
||||
channels: response.body,
|
||||
totalItems: parseInt(response.headers.get('x-total-count'), 10)
|
||||
|
||||
@ -9,44 +9,44 @@
|
||||
<div class="fee-estimation-wrapper" *ngIf="statistics$ | async as statistics; else loadingReward">
|
||||
|
||||
<div class="fee-estimation-container" *ngIf="mode === 'avg'">
|
||||
<div class="item">
|
||||
<div class="item" [class]="!statistics.previous ? 'more-padding' : ''">
|
||||
<h5 class="card-title" i18n="ln.average-capacity">Avg Capacity</h5>
|
||||
<div class="card-text">
|
||||
<div class="fee-text">
|
||||
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||
{{ statistics.latest?.avg_capacity || 0 | number: '1.0-0' }}
|
||||
<span i18n="shared.sat-vbyte|sat/vB">sats</span>
|
||||
</div>
|
||||
<span class="fiat">
|
||||
<span class="fiat" *ngIf="statistics.previous">
|
||||
<app-change [current]="statistics.latest?.avg_capacity" [previous]="statistics.previous?.avg_capacity"></app-change>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<div class="item" [class]="!statistics.previous ? 'more-padding' : ''">
|
||||
<h5 class="card-title" i18n="ln.average-feerate">Avg Fee Rate</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="ln.average-feerate-desc"
|
||||
ngbTooltip="The average fee rate charged by routing nodes, ignoring fee rates > 0.5% or 5000ppm"
|
||||
placement="bottom">
|
||||
<div class="fee-text">
|
||||
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||
{{ statistics.latest?.avg_fee_rate || 0 | number: '1.0-0' }}
|
||||
<span i18n="shared.sat-vbyte|sat/vB">ppm</span>
|
||||
</div>
|
||||
<span class="fiat">
|
||||
<span class="fiat" *ngIf="statistics.previous">
|
||||
<app-change [current]="statistics.latest?.avg_fee_rate" [previous]="statistics.previous?.avg_fee_rate"></app-change>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<div class="item" [class]="!statistics.previous ? 'more-padding' : ''">
|
||||
<h5 class="card-title" i18n="ln.average-basefee">Avg Base Fee</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="ln.average-basefee-desc"
|
||||
ngbTooltip="The average base fee charged by routing nodes, ignoring base fees > 5000ppm" placement="bottom">
|
||||
<div class="card-text">
|
||||
<div class="fee-text">
|
||||
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||
{{ statistics.latest?.avg_base_fee_mtokens || 0 | number: '1.0-0' }}
|
||||
<span i18n="shared.sat-vbyte|sat/vB">msats</span>
|
||||
</div>
|
||||
<span class="fiat">
|
||||
<span class="fiat" *ngIf="statistics.previous">
|
||||
<app-change [current]="statistics.latest?.avg_base_fee_mtokens" [previous]="statistics.previous?.avg_base_fee_mtokens"></app-change>
|
||||
</span>
|
||||
</div>
|
||||
@ -55,43 +55,45 @@
|
||||
</div>
|
||||
|
||||
<div class="fee-estimation-container" *ngIf="mode === 'med'">
|
||||
<div class="item">
|
||||
<div class="item" [class]="!statistics.previous ? 'more-padding' : ''">
|
||||
<h5 class="card-title" i18n="ln.median-capacity">Med Capacity</h5>
|
||||
<div class="card-text">
|
||||
<div class="fee-text">
|
||||
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||
{{ statistics.latest?.med_capacity || 0 | number: '1.0-0' }}
|
||||
<span i18n="shared.sat-vbyte|sat/vB">sats</span>
|
||||
</div>
|
||||
<span class="fiat">
|
||||
<span class="fiat" *ngIf="statistics.previous">
|
||||
<app-change [current]="statistics.latest?.med_capacity" [previous]="statistics.previous?.med_capacity"></app-change>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
|
||||
<div class="item" [class]="!statistics.previous ? 'more-padding' : ''">
|
||||
<h5 class="card-title" i18n="ln.average-feerate">Med Fee Rate</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="ln.median-feerate-desc"
|
||||
ngbTooltip="The average fee rate charged by routing nodes, ignoring fee rates > 0.5% or 5000ppm"
|
||||
ngbTooltip="The median fee rate charged by routing nodes, ignoring fee rates > 0.5% or 5000ppm"
|
||||
placement="bottom">
|
||||
<div class="fee-text">
|
||||
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||
{{ statistics.latest?.med_fee_rate || 0 | number: '1.0-0' }}
|
||||
<span i18n="shared.sat-vbyte|sat/vB">ppm</span>
|
||||
</div>
|
||||
<span class="fiat">
|
||||
<span class="fiat" *ngIf="statistics.previous">
|
||||
<app-change [current]="statistics.latest?.med_fee_rate" [previous]="statistics.previous?.med_fee_rate"></app-change>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
|
||||
<div class="item" [class]="!statistics.previous ? 'more-padding' : ''">
|
||||
<h5 class="card-title" i18n="ln.median-basefee">Med Base Fee</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="ln.median-basefee-desc"
|
||||
ngbTooltip="The median base fee charged by routing nodes, ignoring base fees > 5000ppm" placement="bottom">
|
||||
<div class="card-text">
|
||||
<div class="fee-text">
|
||||
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||
{{ statistics.latest?.med_base_fee_mtokens || 0 | number: '1.0-0' }}
|
||||
<span i18n="shared.sat-vbyte|sat/vB">msats</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="fiat">
|
||||
<span class="fiat" *ngIf="statistics.previous">
|
||||
<app-change [current]="statistics.latest?.med_base_fee_mtokens" [previous]="statistics.previous?.med_base_fee_mtokens"></app-change>
|
||||
</span>
|
||||
</div>
|
||||
@ -102,21 +104,21 @@
|
||||
<ng-template #loadingReward>
|
||||
<div class="fee-estimation-container loading-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.rewards">Nodes</h5>
|
||||
<h5 class="card-title" i18n="ln.average-capacity">Avg Capacity</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.rewards-per-tx">Channels</h5>
|
||||
<h5 class="card-title" i18n="ln.average-feerate">Avg Fee Rate</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.average-fee">Average Channel</h5>
|
||||
<h5 class="card-title" i18n="ln.average-basefee">Avg Base Fee</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
|
||||
@ -18,6 +18,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.fee-estimation-wrapper {
|
||||
min-height: 77px;
|
||||
}
|
||||
|
||||
.fee-estimation-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -30,7 +34,10 @@
|
||||
width: -webkit-fill-available;
|
||||
@media (min-width: 376px) {
|
||||
margin: 0 auto 0px;
|
||||
}
|
||||
}
|
||||
&.more-padding {
|
||||
padding-top: 10px;
|
||||
}
|
||||
&:first-child{
|
||||
display: none;
|
||||
@media (min-width: 485px) {
|
||||
@ -57,6 +64,9 @@
|
||||
margin: auto;
|
||||
line-height: 1.45;
|
||||
padding: 0px 2px;
|
||||
&.no-border {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
.fiat {
|
||||
display: block;
|
||||
|
||||
@ -32,7 +32,7 @@ export class LightningApiService {
|
||||
}
|
||||
|
||||
getChannelsByNodeId$(publicKey: string, index: number = 0, status = 'open'): Observable<any> {
|
||||
let params = new HttpParams()
|
||||
const params = new HttpParams()
|
||||
.set('public_key', publicKey)
|
||||
.set('index', index)
|
||||
.set('status', status)
|
||||
|
||||
@ -29,6 +29,7 @@ import { TopNodesPerChannels } from '../lightning/nodes-ranking/top-nodes-per-ch
|
||||
import { TopNodesPerCapacity } from '../lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component';
|
||||
import { OldestNodes } from '../lightning/nodes-ranking/oldest-nodes/oldest-nodes.component';
|
||||
import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component';
|
||||
import { NodeChannels } from '../lightning/nodes-channels/node-channels.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -56,6 +57,7 @@ import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/no
|
||||
TopNodesPerCapacity,
|
||||
OldestNodes,
|
||||
NodesRankingsDashboard,
|
||||
NodeChannels,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -89,6 +91,7 @@ import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/no
|
||||
TopNodesPerCapacity,
|
||||
OldestNodes,
|
||||
NodesRankingsDashboard,
|
||||
NodeChannels,
|
||||
],
|
||||
providers: [
|
||||
LightningApiService,
|
||||
|
||||
@ -1,129 +1,5 @@
|
||||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.full-container {
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
/* min-height: 500px; */
|
||||
height: calc(100% - 150px);
|
||||
@media (max-width: 992px) {
|
||||
height: 100%;
|
||||
padding-bottom: 100px;
|
||||
};
|
||||
margin-top: 25px;
|
||||
margin-bottom: 25px;
|
||||
min-height: 100%;
|
||||
}
|
||||
/*
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-bottom: 20px;
|
||||
padding-right: 10px;
|
||||
@media (max-width: 992px) {
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
@media (max-width: 829px) {
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
@media (max-width: 629px) {
|
||||
padding-bottom: 55px;
|
||||
}
|
||||
@media (max-width: 567px) {
|
||||
padding-bottom: 55px;
|
||||
}
|
||||
}
|
||||
*/
|
||||
.chart-widget {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 270px;
|
||||
}
|
||||
|
||||
.formRadioGroup {
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@media (min-width: 991px) {
|
||||
position: relative;
|
||||
top: -65px;
|
||||
}
|
||||
@media (min-width: 830px) and (max-width: 991px) {
|
||||
position: relative;
|
||||
top: 0px;
|
||||
}
|
||||
@media (min-width: 830px) {
|
||||
flex-direction: row;
|
||||
float: right;
|
||||
margin-top: 0px;
|
||||
}
|
||||
.btn-sm {
|
||||
font-size: 9px;
|
||||
@media (min-width: 830px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pool-distribution {
|
||||
min-height: 56px;
|
||||
display: block;
|
||||
@media (min-width: 485px) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
h5 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.item {
|
||||
width: 50%;
|
||||
display: inline-block;
|
||||
margin: 0px auto 20px;
|
||||
&:nth-child(2) {
|
||||
order: 2;
|
||||
@media (min-width: 485px) {
|
||||
order: 3;
|
||||
}
|
||||
}
|
||||
&:nth-child(3) {
|
||||
order: 3;
|
||||
@media (min-width: 485px) {
|
||||
order: 2;
|
||||
display: block;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.card-title {
|
||||
font-size: 1rem;
|
||||
color: #4a68b9;
|
||||
}
|
||||
.card-text {
|
||||
font-size: 18px;
|
||||
span {
|
||||
color: #ffffff66;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
display: block;
|
||||
max-width: 80px;
|
||||
margin: 15px auto 3px;
|
||||
}
|
||||
@ -25,7 +25,7 @@ import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
export class NodeStatisticsChartComponent implements OnInit {
|
||||
@Input() publicKey: string;
|
||||
@Input() right: number | string = 65;
|
||||
@Input() left: number | string = 55;
|
||||
@Input() left: number | string = 45;
|
||||
@Input() widget = false;
|
||||
|
||||
miningWindowPreference: string;
|
||||
@ -96,7 +96,7 @@ export class NodeStatisticsChartComponent implements OnInit {
|
||||
],
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 70,
|
||||
bottom: 20,
|
||||
right: this.right,
|
||||
left: this.left,
|
||||
},
|
||||
|
||||
@ -1,76 +1,64 @@
|
||||
<div class="fee-estimation-wrapper" *ngIf="statistics$ | async as statistics; else loadingReward">
|
||||
<div class="fee-estimation-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.average-fee">Capacity</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="mining.average-fee" ngbTooltip="Percentage change past week"
|
||||
placement="bottom">
|
||||
<div class="fee-text">
|
||||
<div class="item" [class]="!statistics.previous ? 'more-padding' : ''">
|
||||
<h5 class="card-title" i18n="lightning.capacity">Capacity</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="mining.percentage-change-last-week" ngbTooltip="Percentage change past week"
|
||||
[disableTooltip]="!statistics.previous" placement="bottom">
|
||||
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||
<app-amount [satoshis]="statistics.latest?.total_capacity" digitsInfo="1.2-2"></app-amount>
|
||||
</div>
|
||||
<span class="fiat">
|
||||
<span class="fiat" *ngIf="statistics.previous">
|
||||
<app-change [current]="statistics.latest?.total_capacity" [previous]="statistics.previous?.total_capacity">
|
||||
</app-change>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.rewards">Nodes</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="mining.rewards-desc" ngbTooltip="Percentage change past week"
|
||||
placement="bottom">
|
||||
<div class="fee-text">
|
||||
<div class="item" [class]="!statistics.previous ? 'more-padding' : ''">
|
||||
<h5 class="card-title" i18n="lightning.nodes">Nodes</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="mining.percentage-change-last-week" ngbTooltip="Percentage change past week"
|
||||
[disableTooltip]="!statistics.previous">
|
||||
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||
{{ statistics.latest?.node_count || 0 | number }}
|
||||
</div>
|
||||
<span class="fiat">
|
||||
<span class="fiat" *ngIf="statistics.previous">
|
||||
<app-change [current]="statistics.latest?.node_count" [previous]="statistics.previous?.node_count"></app-change>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.rewards-per-tx">Channels</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="mining.rewards-per-tx-desc" ngbTooltip="Percentage change past week"
|
||||
placement="bottom">
|
||||
<div class="fee-text">
|
||||
<div class="item" [class]="!statistics.previous ? 'more-padding' : ''">
|
||||
<h5 class="card-title" i18n="lightning.channels">Channels</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="mining.percentage-change-last-week" ngbTooltip="Percentage change past week"
|
||||
[disableTooltip]="!statistics.previous">
|
||||
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||
{{ statistics.latest?.channel_count || 0 | number }}
|
||||
</div>
|
||||
<span class="fiat">
|
||||
<span class="fiat" *ngIf="statistics.previous">
|
||||
<app-change [current]="statistics.latest?.channel_count" [previous]="statistics.previous?.channel_count">
|
||||
</app-change>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.average-fee">Average Channel</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="mining.average-fee"
|
||||
ngbTooltip="Fee paid on average for each transaction in the past 144 blocks" placement="bottom">
|
||||
<app-amount [satoshis]="statistics.latest.average_channel_size" digitsInfo="1.2-3"></app-amount>
|
||||
<span class="fiat">
|
||||
<app-change [current]="statistics.latest.average_channel_size" [previous]="statistics.previous.average_channel_size"></app-change>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingReward>
|
||||
<div class="fee-estimation-container loading-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.rewards">Nodes</h5>
|
||||
<h5 class="card-title" i18n="lightning.nodes">Nodes</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.rewards-per-tx">Channels</h5>
|
||||
<h5 class="card-title" i18n="lightning.channels">Channels</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.average-fee">Average Channel</h5>
|
||||
<h5 class="card-title" i18n="lightning.average-channels">Average Channel</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
|
||||
@ -18,6 +18,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.fee-estimation-wrapper {
|
||||
min-height: 77px;
|
||||
}
|
||||
|
||||
.fee-estimation-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -30,7 +34,10 @@
|
||||
width: -webkit-fill-available;
|
||||
@media (min-width: 376px) {
|
||||
margin: 0 auto 0px;
|
||||
}
|
||||
}
|
||||
&.more-padding {
|
||||
padding-top: 10px;
|
||||
}
|
||||
&:first-child{
|
||||
display: none;
|
||||
@media (min-width: 485px) {
|
||||
@ -57,6 +64,9 @@
|
||||
margin: auto;
|
||||
line-height: 1.45;
|
||||
padding: 0px 2px;
|
||||
&.no-border {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
.fiat {
|
||||
display: block;
|
||||
|
||||
@ -52,7 +52,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md map-col">
|
||||
<app-nodes-channels-map *ngIf="!error" [style]="'nodepage'" [publicKey]="node.public_key" [fitContainer]="true" (readyEvent)="onMapReady()"></app-nodes-channels-map>
|
||||
<app-nodes-channels-map *ngIf="!error" [style]="'nodepage'" [publicKey]="node.public_key" [fitContainer]="true" [placeholder]="true" [hasLocation]="!!node.as_number" (readyEvent)="onMapReady()"></app-nodes-channels-map>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -42,9 +42,9 @@ export class NodePreviewComponent implements OnInit {
|
||||
this.node$ = this.activatedRoute.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.openGraphService.waitFor('node-map');
|
||||
this.openGraphService.waitFor('node-data');
|
||||
this.publicKey = params.get('public_key');
|
||||
this.publicKey = params.get('public_key');
|
||||
this.openGraphService.waitFor('node-map-' + this.publicKey);
|
||||
this.openGraphService.waitFor('node-data-' + this.publicKey);
|
||||
return this.lightningApiService.getNode$(params.get('public_key'));
|
||||
}),
|
||||
map((node) => {
|
||||
@ -75,14 +75,14 @@ export class NodePreviewComponent implements OnInit {
|
||||
this.socketTypes = Object.keys(socketTypesMap);
|
||||
node.avgCapacity = node.capacity / Math.max(1, node.active_channel_count);
|
||||
|
||||
this.openGraphService.waitOver('node-data');
|
||||
this.openGraphService.waitOver('node-data-' + this.publicKey);
|
||||
|
||||
return node;
|
||||
}),
|
||||
catchError(err => {
|
||||
this.error = err;
|
||||
this.openGraphService.fail('node-map');
|
||||
this.openGraphService.fail('node-data');
|
||||
this.openGraphService.fail('node-map-' + this.publicKey);
|
||||
this.openGraphService.fail('node-data-' + this.publicKey);
|
||||
return [{
|
||||
alias: this.publicKey,
|
||||
public_key: this.publicKey,
|
||||
@ -100,6 +100,6 @@ export class NodePreviewComponent implements OnInit {
|
||||
}
|
||||
|
||||
onMapReady() {
|
||||
this.openGraphService.waitOver('node-map');
|
||||
this.openGraphService.waitOver('node-map-' + this.publicKey);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
<div class="container-xl" *ngIf="(node$ | async) as node">
|
||||
<div class="container-xl" *ngIf="(node$ | async) as node; else skeletonLoader">
|
||||
<h5 class="mb-0" style="color: #ffffff66" i18n="lightning.node">Lightning node</h5>
|
||||
<div class="title-container mb-2" *ngIf="!error">
|
||||
<h1 class="mb-0">{{ node.alias }}</h1>
|
||||
<h1 class="mb-0 text-truncate">{{ node.alias }}</h1>
|
||||
<span class="tx-link">
|
||||
<a [routerLink]="['/lightning/node' | relativeUrl, node.public_key]">
|
||||
{{ node.public_key | shortenString : publicKeySize }}
|
||||
<span class="d-inline d-lg-none">{{ node.public_key | shortenString : 24 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ node.public_key }}</span>
|
||||
</a>
|
||||
<app-clipboard [text]="node.public_key"></app-clipboard>
|
||||
</span>
|
||||
@ -21,30 +22,30 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<table class="table table-borderless table-striped">
|
||||
<table class="table table-borderless table-striped table-fixed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="lightning.active-capacity">Active capacity</td>
|
||||
<td i18n="lightning.active-capacity" class="text-truncate label">Active capacity</td>
|
||||
<td>
|
||||
<app-sats [satoshis]="node.capacity"></app-sats>
|
||||
<app-fiat [value]="node.capacity" digitsInfo="1.0-0"></app-fiat>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.active-channels">Active channels</td>
|
||||
<td i18n="lightning.active-channels" class="text-truncate label">Active channels</td>
|
||||
<td>
|
||||
{{ node.active_channel_count }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.active-channels-avg">Average channel size</td>
|
||||
<td i18n="lightning.active-channels-avg" class="text-wrap label">Average channel size</td>
|
||||
<td>
|
||||
<app-sats [satoshis]="node.avgCapacity"></app-sats>
|
||||
<app-fiat [value]="node.avgCapacity" digitsInfo="1.0-0"></app-fiat>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="node.geolocation">
|
||||
<td i18n="location">Location</td>
|
||||
<td i18n="location" class="text-truncate">Location</td>
|
||||
<td>
|
||||
<app-geolocation [data]="node.geolocation" [type]="'node'"></app-geolocation>
|
||||
</td>
|
||||
@ -54,30 +55,30 @@
|
||||
</div>
|
||||
<div class="w-100 d-block d-md-none"></div>
|
||||
<div class="col-md">
|
||||
<table class="table table-borderless table-striped">
|
||||
<table class="table table-borderless table-striped table-fixed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="address.total-received">First seen</td>
|
||||
<td i18n="address.total-received" class="text-truncate label">First seen</td>
|
||||
<td>
|
||||
<app-timestamp [unixTime]="node.first_seen"></app-timestamp>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="address.total-sent">Last update</td>
|
||||
<td i18n="address.total-sent" class="text-truncate label">Last update</td>
|
||||
<td>
|
||||
<app-timestamp [unixTime]="node.updated_at"></app-timestamp>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="address.balance">Color</td>
|
||||
<td i18n="address.balance" class="text-truncate label">Color</td>
|
||||
<td>
|
||||
<div [ngStyle]="{'color': node.color}">{{ node.color }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="node.country">
|
||||
<td i18n="isp">ISP</td>
|
||||
<td i18n="isp" class="text-truncate label">ISP</td>
|
||||
<td>
|
||||
<a [routerLink]="['/lightning/nodes/isp' | relativeUrl, node.as_number]">
|
||||
<a class="d-block text-wrap" [routerLink]="['/lightning/nodes/isp' | relativeUrl, node.as_number]">
|
||||
{{ node.as_organization }} [ASN {{node.as_number}}]
|
||||
</a>
|
||||
</td>
|
||||
@ -114,20 +115,140 @@
|
||||
</div>
|
||||
</button>
|
||||
<button class="btn btn-secondary ml-1" type="button" id="inputGroupFileAddon04">
|
||||
<app-clipboard [text]="node.socketsObject[selectedSocketIndex].socket"></app-clipboard>
|
||||
<app-clipboard [text]="node.socketsObject[selectedSocketIndex].socket" [leftPadding]="false"></app-clipboard>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<app-nodes-channels-map *ngIf="!error" [style]="'nodepage'" [publicKey]="node.public_key"></app-nodes-channels-map>
|
||||
<app-node-statistics-chart [publicKey]="node.public_key" *ngIf="!error"></app-node-statistics-chart>
|
||||
<div *ngIf="!error">
|
||||
<div class="row" *ngIf="node.as_number">
|
||||
<div class="col-sm">
|
||||
<app-nodes-channels-map [style]="'nodepage'" [publicKey]="node.public_key" [hasLocation]="!!node.as_number"></app-nodes-channels-map>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<app-node-statistics-chart [publicKey]="node.public_key"></app-node-statistics-chart>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!node.as_number">
|
||||
<app-node-statistics-chart [publicKey]="node.public_key"></app-node-statistics-chart>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between" *ngIf="!error">
|
||||
<h2>Channels ({{ channelsListStatus === 'open' ? node.opened_channel_count : node.closed_channel_count }})</h2>
|
||||
<app-node-channels style="display:block;margin-bottom: 40px" [publicKey]="node.public_key"></app-node-channels>
|
||||
|
||||
<div class="d-flex">
|
||||
<h2 *ngIf="channelsListStatus === 'open'">
|
||||
<span i18n="lightning.open-channels">Open channels</span>
|
||||
<span> ({{ node.opened_channel_count }})</span>
|
||||
</h2>
|
||||
<h2 *ngIf="channelsListStatus === 'closed'">
|
||||
<span i18n="lightning.open-channels">Closed channels</span>
|
||||
<span> ({{ node.closed_channel_count }})</span>
|
||||
</h2>
|
||||
<div *ngIf="channelListLoading" class="spinner-border ml-3" role="status"></div>
|
||||
</div>
|
||||
|
||||
<app-channels-list [publicKey]="node.public_key"
|
||||
(channelsStatusChangedEvent)="onChannelsListStatusChanged($event)"
|
||||
(loadingEvent)="onLoadingEvent($event)"
|
||||
></app-channels-list>
|
||||
</div>
|
||||
|
||||
<app-channels-list *ngIf="!error" [publicKey]="node.public_key"
|
||||
(channelsStatusChangedEvent)="onChannelsListStatusChanged($event)"></app-channels-list>
|
||||
|
||||
</div>
|
||||
|
||||
<ng-template #skeletonLoader>
|
||||
<div class="container-xl">
|
||||
<h5 class="mb-0" style="color: #ffffff66" i18n="lightning.node">Lightning node</h5>
|
||||
<div class="title-container mb-2">
|
||||
<h1 class="mb-0"><span class="skeleton-loader" style="width: 250px; height: 36px; margin-top: 5px; margin-bottom: 5px;"></span></h1>
|
||||
<span class="tx-link">
|
||||
<span class="skeleton-loader" style="margin-bottom: 3px; width: 80%;"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="box">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<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>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="w-100 d-block d-md-none"></div>
|
||||
<div class="col-md">
|
||||
<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>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="input-group mt-3" >
|
||||
<span class="input-group-text" id="basic-addon3"><span class="skeleton-loader" style="width: 75px;"></span></span>
|
||||
<input type="text" class="form-control" disabled style="opacity: 0.3;">
|
||||
<button class="btn btn-secondary ml-1" type="button" id="inputGroupFileAddon04" [disabled]="true">
|
||||
<fa-icon [icon]="['fas', 'qrcode']" [fixedWidth]="true"></fa-icon>
|
||||
</button>
|
||||
<button class="btn btn-secondary ml-1" type="button" id="inputGroupFileAddon04" [disabled]="true">
|
||||
<app-clipboard [text]="''"></app-clipboard>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div style="height: 400px;">
|
||||
<div class="text-center loadingGraphs">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div style="height: 400px;">
|
||||
<div class="text-center loadingGraphs">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<br>
|
||||
|
||||
@ -1,29 +1,11 @@
|
||||
.title-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tx-link {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
@media (min-width: 650px) {
|
||||
align-self: end;
|
||||
margin-left: 15px;
|
||||
margin-top: 0px;
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
margin-bottom: 4px;
|
||||
top: 1px;
|
||||
position: relative;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
order: 2;
|
||||
}
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.qr-wrapper {
|
||||
@ -56,4 +38,37 @@ app-fiat {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spinner-border {
|
||||
@media (min-width: 768px) {
|
||||
margin-top: 6.5px;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
margin-top: 2.3px;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 50%;
|
||||
@media (min-width: 576px) {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
h1 > .skeleton-loader {
|
||||
height: 28px !important;
|
||||
};
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@ import { Observable } from 'rxjs';
|
||||
import { catchError, map, switchMap } from 'rxjs/operators';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { isMobile } from '../../shared/common.utils';
|
||||
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
|
||||
|
||||
@Component({
|
||||
@ -22,18 +21,13 @@ export class NodeComponent implements OnInit {
|
||||
channelsListStatus: string;
|
||||
error: Error;
|
||||
publicKey: string;
|
||||
|
||||
publicKeySize = 99;
|
||||
channelListLoading = false;
|
||||
|
||||
constructor(
|
||||
private lightningApiService: LightningApiService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private seoService: SeoService,
|
||||
) {
|
||||
if (isMobile()) {
|
||||
this.publicKeySize = 12;
|
||||
}
|
||||
}
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.node$ = this.activatedRoute.paramMap
|
||||
@ -97,4 +91,8 @@ export class NodeComponent implements OnInit {
|
||||
onChannelsListStatusChanged(e) {
|
||||
this.channelsListStatus = e;
|
||||
}
|
||||
|
||||
onLoadingEvent(e) {
|
||||
this.channelListLoading = e;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<div [style]="style === 'widget' ? 'height: 250px' : ''">
|
||||
<div *ngIf="channelsObservable | async">
|
||||
<div class="map-wrapper" [class]="style">
|
||||
<ng-container *ngIf="channelsObservable | async">
|
||||
<div *ngIf="chartOptions" [class]="'full-container ' + style + (fitContainer ? ' fit-container' : '')">
|
||||
<div *ngIf="style === 'graph'" class="card-header">
|
||||
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
|
||||
@ -8,15 +8,15 @@
|
||||
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
|
||||
</div>
|
||||
|
||||
<div class="chart" [class]="style" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"
|
||||
(chartFinished)="onChartFinished($event)">
|
||||
<div class="chart" [class]="style" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
|
||||
</div>
|
||||
|
||||
<div *ngIf="!chartOptions && style === 'nodepage'" style="padding-top: 30px"></div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!chartOptions && style === 'nodepage'" style="padding-top: 30px"></div>
|
||||
</div>
|
||||
|
||||
<div class="text-center loading-spinner" [class]="style" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center loading-spinner" [class]="style" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
@ -1,3 +1,15 @@
|
||||
.map-wrapper {
|
||||
height: 100%;
|
||||
|
||||
&.widget {
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
&.graph {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
border-bottom: 0;
|
||||
font-size: 18px;
|
||||
@ -12,11 +24,6 @@
|
||||
width: 100%;
|
||||
min-height: 600px;
|
||||
height: calc(100% - 150px);
|
||||
|
||||
@media (max-width: 992px) {
|
||||
height: 100%;
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
}
|
||||
.full-container.nodepage {
|
||||
min-height: 400px;
|
||||
@ -27,6 +34,7 @@
|
||||
min-height: 400px;
|
||||
margin-top: 25px;
|
||||
margin-bottom: 25px;
|
||||
min-height: 100%;
|
||||
}
|
||||
.full-container.widget {
|
||||
height: 250px;
|
||||
@ -68,21 +76,21 @@
|
||||
min-height: 600px;
|
||||
}
|
||||
.chart.nodepage {
|
||||
min-height: 400px;
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
.chart.channelpage {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.widget {
|
||||
width: 90vw;
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
height: 250px;
|
||||
-webkit-mask: linear-gradient(0deg, #11131f00 5%, #11131fff 25%);
|
||||
@media (max-width: 767.98px) {
|
||||
width: 100vw;
|
||||
}
|
||||
}
|
||||
.widget > .chart {
|
||||
min-height: 250px;
|
||||
@ -97,6 +105,9 @@
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
@media (max-width: 767.98px) {
|
||||
top: 550px;
|
||||
}
|
||||
}
|
||||
.loading-spinner.widget {
|
||||
position: absolute;
|
||||
@ -108,3 +119,21 @@
|
||||
top: 250px;
|
||||
}
|
||||
}
|
||||
.loading-spinner.nodepage {
|
||||
position: absolute;
|
||||
top: 200px;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.loading-spinner.channelpage {
|
||||
position: absolute;
|
||||
top: 400px;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
@media (max-width: 767.98px) {
|
||||
top: 450px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,6 +21,8 @@ export class NodesChannelsMap implements OnInit {
|
||||
@Input() publicKey: string | undefined;
|
||||
@Input() channel: any[] = [];
|
||||
@Input() fitContainer = false;
|
||||
@Input() hasLocation = true;
|
||||
@Input() placeholder = false;
|
||||
@Output() readyEvent = new EventEmitter();
|
||||
|
||||
channelsObservable: Observable<any>;
|
||||
@ -32,7 +34,7 @@ export class NodesChannelsMap implements OnInit {
|
||||
channelColor = '#466d9d';
|
||||
channelCurve = 0;
|
||||
nodeSize = 4;
|
||||
isLoading = true;
|
||||
isLoading = false;
|
||||
|
||||
chartInstance = undefined;
|
||||
chartOptions: EChartsOption = {};
|
||||
@ -73,6 +75,11 @@ export class NodesChannelsMap implements OnInit {
|
||||
this.channelsObservable = this.activatedRoute.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.isLoading = true;
|
||||
if (this.style === 'channelpage' && this.channel.length === 0 || !this.hasLocation) {
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
return zip(
|
||||
this.assetsService.getWorldMapJson$,
|
||||
this.style !== 'channelpage' ? this.apiService.getChannelsGeo$(params.get('public_key') ?? undefined, this.style) : [''],
|
||||
@ -165,7 +172,7 @@ export class NodesChannelsMap implements OnInit {
|
||||
|
||||
if (this.style === 'nodepage' && thisNodeGPS) {
|
||||
this.center = [thisNodeGPS[0], thisNodeGPS[1]];
|
||||
this.zoom = 10;
|
||||
this.zoom = 5;
|
||||
this.channelWidth = 1;
|
||||
this.channelOpacity = 1;
|
||||
}
|
||||
@ -195,11 +202,26 @@ export class NodesChannelsMap implements OnInit {
|
||||
|
||||
prepareChartOptions(nodes, channels) {
|
||||
let title: object;
|
||||
if (channels.length === 0) {
|
||||
if (channels.length === 0 && !this.placeholder) {
|
||||
this.chartOptions = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// empty map fallback
|
||||
if (channels.length === 0 && this.placeholder) {
|
||||
title = {
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
fontSize: 18
|
||||
},
|
||||
text: $localize`No geolocation data available`,
|
||||
left: 'center',
|
||||
top: 'center'
|
||||
};
|
||||
this.zoom = 1.5;
|
||||
this.center = [0, 20];
|
||||
}
|
||||
|
||||
this.chartOptions = {
|
||||
silent: this.style === 'widget',
|
||||
title: title ?? undefined,
|
||||
@ -216,7 +238,7 @@ export class NodesChannelsMap implements OnInit {
|
||||
roam: this.style === 'widget' ? false : true,
|
||||
itemStyle: {
|
||||
borderColor: 'black',
|
||||
color: '#ffffff44'
|
||||
color: '#272b3f'
|
||||
},
|
||||
scaleLimit: {
|
||||
min: 1.3,
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
<div *ngIf="channelsObservable$ | async" style="min-height: 455px">
|
||||
<h2 i18n="lightning.active-channels-map">Active channels map</h2>
|
||||
<div echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isLoading" class="text-center loading-spinner">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
@ -0,0 +1,9 @@
|
||||
.loading-spinner {
|
||||
min-height: 455px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.spinner-border {
|
||||
position: relative;
|
||||
top: 225px;
|
||||
}
|
||||
@ -0,0 +1,147 @@
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ECharts, EChartsOption, TreemapSeriesOption } from 'echarts';
|
||||
import { Observable, share, switchMap, tap } from 'rxjs';
|
||||
import { lerpColor } from 'src/app/shared/graphs.utils';
|
||||
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-node-channels',
|
||||
templateUrl: './node-channels.component.html',
|
||||
styleUrls: ['./node-channels.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NodeChannels implements OnChanges {
|
||||
@Input() publicKey: string;
|
||||
|
||||
chartInstance: ECharts;
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
};
|
||||
|
||||
channelsObservable$: Observable<any>;
|
||||
isLoading = true;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private lightningApiService: LightningApiService,
|
||||
private amountShortenerPipe: AmountShortenerPipe,
|
||||
private zone: NgZone,
|
||||
private router: Router,
|
||||
private stateService: StateService,
|
||||
) {}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.prepareChartOptions(null);
|
||||
|
||||
this.channelsObservable$ = this.lightningApiService.getChannelsByNodeId$(this.publicKey, -1, 'active')
|
||||
.pipe(
|
||||
switchMap((response) => {
|
||||
this.isLoading = true;
|
||||
if ((response.body?.length ?? 0) <= 0) {
|
||||
this.isLoading = false;
|
||||
return [''];
|
||||
}
|
||||
return [response.body];
|
||||
}),
|
||||
tap((body: any[]) => {
|
||||
if (body.length === 0 || body[0].length === 0) {
|
||||
return;
|
||||
}
|
||||
const biggestCapacity = body[0].capacity;
|
||||
this.prepareChartOptions(body.map(channel => {
|
||||
return {
|
||||
name: channel.node.alias,
|
||||
value: channel.capacity,
|
||||
shortId: channel.short_id,
|
||||
id: channel.id,
|
||||
itemStyle: {
|
||||
color: lerpColor('#1E88E5', '#D81B60', Math.pow(channel.capacity / biggestCapacity, 0.4)),
|
||||
}
|
||||
};
|
||||
}));
|
||||
this.isLoading = false;
|
||||
}),
|
||||
share(),
|
||||
);
|
||||
}
|
||||
|
||||
prepareChartOptions(data): void {
|
||||
this.chartOptions = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
textStyle: {
|
||||
align: 'left',
|
||||
}
|
||||
},
|
||||
series: <TreemapSeriesOption[]>[
|
||||
{
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
roam: false,
|
||||
type: 'treemap',
|
||||
data: data,
|
||||
nodeClick: 'link',
|
||||
progressive: 100,
|
||||
tooltip: {
|
||||
show: true,
|
||||
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||
borderRadius: 4,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
textStyle: {
|
||||
color: '#b1b1b1',
|
||||
},
|
||||
borderColor: '#000',
|
||||
formatter: (value): string => {
|
||||
if (value.data.name === undefined) {
|
||||
return ``;
|
||||
}
|
||||
let capacity = '';
|
||||
if (value.data.value > 100000000) {
|
||||
capacity = formatNumber(Math.round(value.data.value / 100000000), this.locale, '1.2-2') + ' BTC';
|
||||
} else {
|
||||
capacity = <string>this.amountShortenerPipe.transform(value.data.value, 2) + ' sats';
|
||||
}
|
||||
|
||||
return `
|
||||
<b style="color: white; margin-left: 2px">${value.data.shortId}</b><br>
|
||||
<span>Node: ${value.name}</span><br>
|
||||
<span>Capacity: ${capacity}</span>
|
||||
`;
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
borderColor: 'black',
|
||||
borderWidth: 1,
|
||||
},
|
||||
breadcrumb: {
|
||||
show: false,
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
onChartInit(ec: ECharts): void {
|
||||
this.chartInstance = ec;
|
||||
|
||||
this.chartInstance.on('click', (e) => {
|
||||
//@ts-ignore
|
||||
if (!e.data.id) {
|
||||
return;
|
||||
}
|
||||
this.zone.run(() => {
|
||||
//@ts-ignore
|
||||
const url = new RelativeUrlPipe(this.stateService).transform(`/lightning/channel/${e.data.id}`);
|
||||
this.router.navigate([url]);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -60,7 +60,7 @@
|
||||
flex-direction: column;
|
||||
@media (min-width: 991px) {
|
||||
position: relative;
|
||||
top: -65px;
|
||||
top: -100px;
|
||||
}
|
||||
@media (min-width: 830px) and (max-width: 991px) {
|
||||
position: relative;
|
||||
|
||||
@ -121,7 +121,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
};
|
||||
} else if (data.tor_nodes.length > 0) {
|
||||
} else if (this.widget && data.tor_nodes.length > 0) {
|
||||
title = {
|
||||
textStyle: {
|
||||
color: 'grey',
|
||||
@ -300,7 +300,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
{ offset: 1, color: '#D81B60AA' },
|
||||
]),
|
||||
|
||||
smooth: true,
|
||||
smooth: false,
|
||||
},
|
||||
{
|
||||
zlevel: 1,
|
||||
@ -321,7 +321,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
{ offset: 0, color: '#FFB300' },
|
||||
{ offset: 1, color: '#FFB300AA' },
|
||||
]),
|
||||
smooth: true,
|
||||
smooth: false,
|
||||
},
|
||||
{
|
||||
zlevel: 1,
|
||||
@ -342,7 +342,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
{ offset: 0, color: '#7D4698' },
|
||||
{ offset: 1, color: '#7D4698AA' },
|
||||
]),
|
||||
smooth: true,
|
||||
smooth: false,
|
||||
},
|
||||
],
|
||||
dataZoom: this.widget ? null : [{
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
|
||||
<div class="card-header" *ngIf="!widget">
|
||||
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
|
||||
<span i18n="lightning.top-100-isp-ln">Top 100 ISP hosting LN nodes</span>
|
||||
<span i18n="lightning.top-100-isp-ln">Top 100 ISPs hosting LN nodes</span>
|
||||
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
|
||||
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
|
||||
</button>
|
||||
|
||||
@ -40,7 +40,7 @@
|
||||
flex-direction: column;
|
||||
@media (min-width: 991px) {
|
||||
position: relative;
|
||||
top: -65px;
|
||||
top: -100px;
|
||||
}
|
||||
@media (min-width: 830px) and (max-width: 991px) {
|
||||
position: relative;
|
||||
|
||||
@ -60,7 +60,7 @@
|
||||
flex-direction: column;
|
||||
@media (min-width: 991px) {
|
||||
position: relative;
|
||||
top: -65px;
|
||||
top: -100px;
|
||||
}
|
||||
@media (min-width: 830px) and (max-width: 991px) {
|
||||
position: relative;
|
||||
|
||||
@ -113,7 +113,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
left: 'center',
|
||||
top: 'center'
|
||||
};
|
||||
} else if (data.channel_count.length > 0) {
|
||||
} else if (this.widget && data.channel_count.length > 0) {
|
||||
title = {
|
||||
textStyle: {
|
||||
color: 'grey',
|
||||
@ -274,7 +274,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
smooth: true,
|
||||
smooth: false,
|
||||
},
|
||||
{
|
||||
zlevel: 0,
|
||||
@ -288,7 +288,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
opacity: 0.5,
|
||||
},
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
smooth: false,
|
||||
}
|
||||
],
|
||||
dataZoom: this.widget ? null : [{
|
||||
|
||||
@ -242,12 +242,12 @@ export class ApiService {
|
||||
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + `/api/v1/enterprise/info/` + name);
|
||||
}
|
||||
|
||||
getChannelByTxIds$(txIds: string[]): Observable<{ inputs: any[], outputs: any[] }> {
|
||||
getChannelByTxIds$(txIds: string[]): Observable<any[]> {
|
||||
let params = new HttpParams();
|
||||
txIds.forEach((txId: string) => {
|
||||
params = params.append('txId[]', txId);
|
||||
});
|
||||
return this.httpClient.get<{ inputs: any[], outputs: any[] }>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels/txids/', { params });
|
||||
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels/txids/', { params });
|
||||
}
|
||||
|
||||
lightningSearch$(searchText: string): Observable<any[]> {
|
||||
|
||||
@ -83,13 +83,13 @@ export class OpenGraphService {
|
||||
waitOver(event) {
|
||||
if (this.previewLoadingEvents[event]) {
|
||||
this.previewLoadingEvents[event]--;
|
||||
if (this.previewLoadingEvents[event] === 0) {
|
||||
if (this.previewLoadingEvents[event] === 0 && this.previewLoadingCount > 0) {
|
||||
delete this.previewLoadingEvents[event]
|
||||
this.previewLoadingCount--;
|
||||
}
|
||||
}
|
||||
if (this.previewLoadingCount === 0) {
|
||||
this.metaService.updateTag({ property: 'og:preview:ready', content: 'ready'});
|
||||
if (this.previewLoadingCount === 0) {
|
||||
this.metaService.updateTag({ property: 'og:preview:ready', content: 'ready'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user