mirror of
https://github.com/mempool/mempool.git
synced 2024-11-20 10:21:52 +01:00
Merge branch 'master' into frontend_runtime_config
This commit is contained in:
commit
847aa1ba13
@ -4,7 +4,7 @@ import logger from '../logger';
|
||||
import { Common } from './common';
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 40;
|
||||
private static currentVersion = 41;
|
||||
private queryTimeout = 120000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
@ -348,6 +348,10 @@ class DatabaseMigration {
|
||||
await this.$executeQuery('ALTER TABLE `nodes` ADD channels int(11) unsigned DEFAULT NULL');
|
||||
await this.$executeQuery('ALTER TABLE `nodes` ADD INDEX `capacity` (`capacity`);');
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 41 && isBitcoin === true) {
|
||||
await this.$executeQuery('UPDATE channels SET closing_reason = NULL WHERE closing_reason = 1');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -129,6 +129,56 @@ class NodesApi {
|
||||
}
|
||||
}
|
||||
|
||||
public async $getFeeHistogram(node_public_key: string): Promise<unknown> {
|
||||
try {
|
||||
const inQuery = `
|
||||
SELECT CASE WHEN fee_rate <= 10.0 THEN CEIL(fee_rate)
|
||||
WHEN (fee_rate > 10.0 and fee_rate <= 100.0) THEN CEIL(fee_rate / 10.0) * 10.0
|
||||
WHEN (fee_rate > 100.0 and fee_rate <= 1000.0) THEN CEIL(fee_rate / 100.0) * 100.0
|
||||
WHEN fee_rate > 1000.0 THEN CEIL(fee_rate / 1000.0) * 1000.0
|
||||
END as bucket,
|
||||
count(short_id) as count,
|
||||
sum(capacity) as capacity
|
||||
FROM (
|
||||
SELECT CASE WHEN node1_public_key = ? THEN node2_fee_rate WHEN node2_public_key = ? THEN node1_fee_rate END as fee_rate,
|
||||
short_id as short_id,
|
||||
capacity as capacity
|
||||
FROM channels
|
||||
WHERE status = 1 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?)
|
||||
) as fee_rate_table
|
||||
GROUP BY bucket;
|
||||
`;
|
||||
const [inRows]: any[] = await DB.query(inQuery, [node_public_key, node_public_key, node_public_key, node_public_key]);
|
||||
|
||||
const outQuery = `
|
||||
SELECT CASE WHEN fee_rate <= 10.0 THEN CEIL(fee_rate)
|
||||
WHEN (fee_rate > 10.0 and fee_rate <= 100.0) THEN CEIL(fee_rate / 10.0) * 10.0
|
||||
WHEN (fee_rate > 100.0 and fee_rate <= 1000.0) THEN CEIL(fee_rate / 100.0) * 100.0
|
||||
WHEN fee_rate > 1000.0 THEN CEIL(fee_rate / 1000.0) * 1000.0
|
||||
END as bucket,
|
||||
count(short_id) as count,
|
||||
sum(capacity) as capacity
|
||||
FROM (
|
||||
SELECT CASE WHEN node1_public_key = ? THEN node1_fee_rate WHEN node2_public_key = ? THEN node2_fee_rate END as fee_rate,
|
||||
short_id as short_id,
|
||||
capacity as capacity
|
||||
FROM channels
|
||||
WHERE status = 1 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?)
|
||||
) as fee_rate_table
|
||||
GROUP BY bucket;
|
||||
`;
|
||||
const [outRows]: any[] = await DB.query(outQuery, [node_public_key, node_public_key, node_public_key, node_public_key]);
|
||||
|
||||
return {
|
||||
incoming: inRows.length > 0 ? inRows : [],
|
||||
outgoing: outRows.length > 0 ? outRows : [],
|
||||
};
|
||||
} catch (e) {
|
||||
logger.err(`Cannot get node fee distribution for ${node_public_key}. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async $getAllNodes(): Promise<any> {
|
||||
try {
|
||||
const query = `SELECT * FROM nodes`;
|
||||
|
@ -20,6 +20,7 @@ class NodesRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings/connectivity', this.$getTopNodesByChannels)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings/age', this.$getOldestNodes)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/statistics', this.$getHistoricalNodeStats)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/fees/histogram', this.$getFeeHistogram)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key', this.$getNode)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/group/:name', this.$getNodeGroup)
|
||||
;
|
||||
@ -95,6 +96,22 @@ class NodesRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async $getFeeHistogram(req: Request, res: Response) {
|
||||
try {
|
||||
const node = await nodesApi.$getFeeHistogram(req.params.public_key);
|
||||
if (!node) {
|
||||
res.status(404).send('Node not found');
|
||||
return;
|
||||
}
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(node);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $getNodesRanking(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const topCapacityNodes = await nodesApi.$getTopCapacityNodes(false);
|
||||
|
@ -70,6 +70,8 @@ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): P
|
||||
logger.info(`Building partial channels from clightning output. Channels processed: ${channelProcessed + 1} of ${keys.length}`);
|
||||
loggerTimer = new Date().getTime() / 1000;
|
||||
}
|
||||
|
||||
channelProcessed++;
|
||||
}
|
||||
|
||||
return consolidatedChannelList;
|
||||
|
@ -289,6 +289,24 @@ class NetworkSyncService {
|
||||
1. Mutually closed
|
||||
2. Forced closed
|
||||
3. Forced closed with penalty
|
||||
|
||||
┌────────────────────────────────────┐ ┌────────────────────────────┐
|
||||
│ outputs contain revocation script? ├──yes──► force close w/ penalty = 3 │
|
||||
└──────────────┬─────────────────────┘ └────────────────────────────┘
|
||||
no
|
||||
┌──────────────▼──────────────────────────┐
|
||||
│ outputs contain other lightning script? ├──┐
|
||||
└──────────────┬──────────────────────────┘ │
|
||||
no yes
|
||||
┌──────────────▼─────────────┐ │
|
||||
│ sequence starts with 0x80 │ ┌────────▼────────┐
|
||||
│ and ├──────► force close = 2 │
|
||||
│ locktime starts with 0x20? │ └─────────────────┘
|
||||
└──────────────┬─────────────┘
|
||||
no
|
||||
┌─────────▼────────┐
|
||||
│ mutual close = 1 │
|
||||
└──────────────────┘
|
||||
*/
|
||||
|
||||
private async $runClosedChannelsForensics(): Promise<void> {
|
||||
@ -326,36 +344,31 @@ class NetworkSyncService {
|
||||
lightningScriptReasons.push(lightningScript);
|
||||
}
|
||||
}
|
||||
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 filteredReasons = lightningScriptReasons.filter((r) => r !== 1);
|
||||
if (filteredReasons.length) {
|
||||
if (filteredReasons.some((r) => r === 2 || r === 4)) {
|
||||
reason = 3;
|
||||
} 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
|
||||
*/
|
||||
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;
|
||||
}
|
||||
reason = 2;
|
||||
}
|
||||
} 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
|
||||
*/
|
||||
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) {
|
||||
|
@ -113,7 +113,7 @@ https://www.transifex.com/mempool/mempool/dashboard/
|
||||
* French @Bayernatoor
|
||||
* Korean @kcalvinalvinn
|
||||
* Italian @HodlBits
|
||||
* Hebrew @Sh0ham
|
||||
* Hebrew @rapidlab309
|
||||
* Georgian @wyd_idk
|
||||
* Hungarian @btcdragonlord
|
||||
* Dutch @m__btc
|
||||
|
@ -74,12 +74,14 @@ let routes: Routes = [
|
||||
children: [],
|
||||
component: AddressComponent,
|
||||
data: {
|
||||
ogImage: true
|
||||
ogImage: true,
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
component: StartComponent,
|
||||
data: { networkSpecific: true },
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
@ -90,6 +92,7 @@ let routes: Routes = [
|
||||
{
|
||||
path: 'block',
|
||||
component: StartComponent,
|
||||
data: { networkSpecific: true },
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
@ -102,6 +105,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'block-audit',
|
||||
data: { networkSpecific: true },
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
@ -121,12 +125,13 @@ let routes: Routes = [
|
||||
{
|
||||
path: 'lightning',
|
||||
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule),
|
||||
data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true },
|
||||
data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true, networks: ['bitcoin'] },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: StatusViewComponent
|
||||
},
|
||||
{
|
||||
@ -185,11 +190,13 @@ let routes: Routes = [
|
||||
children: [],
|
||||
component: AddressComponent,
|
||||
data: {
|
||||
ogImage: true
|
||||
ogImage: true,
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
@ -200,6 +207,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'block',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
@ -213,6 +221,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'block-audit',
|
||||
data: { networkSpecific: true },
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
@ -230,12 +239,14 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'lightning',
|
||||
data: { networks: ['bitcoin'] },
|
||||
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: StatusViewComponent
|
||||
},
|
||||
{
|
||||
@ -291,11 +302,13 @@ let routes: Routes = [
|
||||
children: [],
|
||||
component: AddressComponent,
|
||||
data: {
|
||||
ogImage: true
|
||||
ogImage: true,
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
@ -306,6 +319,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'block',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
@ -319,6 +333,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'block-audit',
|
||||
data: { networkSpecific: true },
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
@ -336,6 +351,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'lightning',
|
||||
data: { networks: ['bitcoin'] },
|
||||
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
|
||||
},
|
||||
],
|
||||
@ -359,6 +375,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: StatusViewComponent
|
||||
},
|
||||
{
|
||||
@ -422,11 +439,13 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
children: [],
|
||||
component: AddressComponent,
|
||||
data: {
|
||||
ogImage: true
|
||||
ogImage: true,
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
@ -437,6 +456,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
},
|
||||
{
|
||||
path: 'block',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
@ -450,18 +470,22 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
},
|
||||
{
|
||||
path: 'assets',
|
||||
data: { networks: ['liquid'] },
|
||||
component: AssetsNavComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'all',
|
||||
data: { networks: ['liquid'] },
|
||||
component: AssetsComponent,
|
||||
},
|
||||
{
|
||||
path: 'asset/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: AssetComponent
|
||||
},
|
||||
{
|
||||
path: 'group/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: AssetGroupComponent
|
||||
},
|
||||
{
|
||||
@ -482,6 +506,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: StatusViewComponent
|
||||
},
|
||||
{
|
||||
@ -532,11 +557,13 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
children: [],
|
||||
component: AddressComponent,
|
||||
data: {
|
||||
ogImage: true
|
||||
ogImage: true,
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
@ -547,6 +574,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
},
|
||||
{
|
||||
path: 'block',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
@ -560,22 +588,27 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
},
|
||||
{
|
||||
path: 'assets',
|
||||
data: { networks: ['liquid'] },
|
||||
component: AssetsNavComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'featured',
|
||||
data: { networkSpecific: true },
|
||||
component: AssetsFeaturedComponent,
|
||||
},
|
||||
{
|
||||
path: 'all',
|
||||
data: { networks: ['liquid'] },
|
||||
component: AssetsComponent,
|
||||
},
|
||||
{
|
||||
path: 'asset/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: AssetComponent
|
||||
},
|
||||
{
|
||||
path: 'group/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: AssetGroupComponent
|
||||
},
|
||||
{
|
||||
@ -609,6 +642,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
data: { networks: ['bitcoin', 'liquid']},
|
||||
component: StatusViewComponent
|
||||
},
|
||||
{
|
||||
|
@ -20,14 +20,17 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'markets',
|
||||
data: { networks: ['bisq'] },
|
||||
component: BisqDashboardComponent,
|
||||
},
|
||||
{
|
||||
path: 'transactions',
|
||||
data: { networks: ['bisq'] },
|
||||
component: BisqTransactionsComponent
|
||||
},
|
||||
{
|
||||
path: 'market/:pair',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqMarketComponent,
|
||||
},
|
||||
{
|
||||
@ -36,6 +39,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'tx/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqTransactionComponent
|
||||
},
|
||||
{
|
||||
@ -45,14 +49,17 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'block/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqBlockComponent,
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqAddressComponent,
|
||||
},
|
||||
{
|
||||
path: 'stats',
|
||||
data: { networks: ['bisq'] },
|
||||
component: BisqStatsComponent,
|
||||
},
|
||||
{
|
||||
|
@ -44,13 +44,13 @@
|
||||
<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"><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>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '/')" 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 + (networkPaths['signet'] || '/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 + (networkPaths['testnet'] || '/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="/"><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>
|
||||
<a ngbDropdownItem class="mainnet active" [routerLink]="networkPaths['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 + (networkPaths['liquid'] || '/')" 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 + (networkPaths['liquidtestnet'] || '/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>
|
||||
|
||||
|
@ -3,6 +3,7 @@ import { Env, StateService } from '../../services/state.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { LanguageService } from '../../services/language.service';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
import { NavigationService } from '../../services/navigation.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-master-page',
|
||||
@ -15,17 +16,23 @@ export class BisqMasterPageComponent implements OnInit {
|
||||
env: Env;
|
||||
isMobile = window.innerWidth <= 767.98;
|
||||
urlLanguage: string;
|
||||
networkPaths: { [network: string]: string };
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private languageService: LanguageService,
|
||||
private enterpriseService: EnterpriseService,
|
||||
private navigationService: NavigationService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.env = this.stateService.env;
|
||||
this.connectionState$ = this.stateService.connectionState$;
|
||||
this.urlLanguage = this.languageService.getLanguageForUrl();
|
||||
this.navigationService.subnetPaths.subscribe((paths) => {
|
||||
console.log('network paths updated...');
|
||||
this.networkPaths = paths;
|
||||
});
|
||||
}
|
||||
|
||||
collapse(): void {
|
||||
|
@ -49,13 +49,13 @@
|
||||
<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"><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>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '')" 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 + (networkPaths['signet'] || '/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 + (networkPaths['testnet'] || '/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"><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>
|
||||
<a [href]="env.BISQ_WEBSITE_URL + urlLanguage + (networkPaths['bisq'] || '')" 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]="networkPaths['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 ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquidtestnet'" [routerLink]="networkPaths['liquidtestnet'] || '/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>
|
||||
|
||||
|
@ -3,6 +3,7 @@ import { Env, StateService } from '../../services/state.service';
|
||||
import { merge, Observable, of} from 'rxjs';
|
||||
import { LanguageService } from '../../services/language.service';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
import { NavigationService } from '../../services/navigation.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-liquid-master-page',
|
||||
@ -17,11 +18,13 @@ export class LiquidMasterPageComponent implements OnInit {
|
||||
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
||||
network$: Observable<string>;
|
||||
urlLanguage: string;
|
||||
networkPaths: { [network: string]: string };
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private languageService: LanguageService,
|
||||
private enterpriseService: EnterpriseService,
|
||||
private navigationService: NavigationService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
@ -29,6 +32,10 @@ export class LiquidMasterPageComponent implements OnInit {
|
||||
this.connectionState$ = this.stateService.connectionState$;
|
||||
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
||||
this.urlLanguage = this.languageService.getLanguageForUrl();
|
||||
this.navigationService.subnetPaths.subscribe((paths) => {
|
||||
console.log('network paths updated...');
|
||||
this.networkPaths = paths;
|
||||
});
|
||||
}
|
||||
|
||||
collapse(): void {
|
||||
|
@ -22,13 +22,13 @@
|
||||
<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="/"><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>
|
||||
<a ngbDropdownItem class="mainnet" [routerLink]="networkPaths['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 ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" [routerLink]="networkPaths['signet'] || '/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]="networkPaths['testnet'] || '/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"><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>
|
||||
<a [href]="env.BISQ_WEBSITE_URL + urlLanguage + (networkPaths['bisq'] || '')" 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 + (networkPaths['liquid'] || '')" 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 + (networkPaths['liquidtestnet'] || '/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>
|
||||
|
||||
|
@ -3,6 +3,7 @@ import { Env, StateService } from '../../services/state.service';
|
||||
import { Observable, merge, of } from 'rxjs';
|
||||
import { LanguageService } from '../../services/language.service';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
import { NavigationService } from '../../services/navigation.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-master-page',
|
||||
@ -18,11 +19,13 @@ export class MasterPageComponent implements OnInit {
|
||||
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
||||
urlLanguage: string;
|
||||
subdomain = '';
|
||||
networkPaths: { [network: string]: string };
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private languageService: LanguageService,
|
||||
private enterpriseService: EnterpriseService,
|
||||
private navigationService: NavigationService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
@ -31,6 +34,10 @@ export class MasterPageComponent implements OnInit {
|
||||
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
||||
this.urlLanguage = this.languageService.getLanguageForUrl();
|
||||
this.subdomain = this.enterpriseService.getSubdomain();
|
||||
this.navigationService.subnetPaths.subscribe((paths) => {
|
||||
console.log('network paths updated...');
|
||||
this.networkPaths = paths;
|
||||
});
|
||||
}
|
||||
|
||||
collapse(): void {
|
||||
|
@ -3,8 +3,8 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { AssetsService } from '../../services/assets.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Observable, of, Subject, zip, BehaviorSubject } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, switchMap, catchError, map } from 'rxjs/operators';
|
||||
import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, switchMap, catchError, map, startWith, tap } from 'rxjs/operators';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
@ -24,7 +24,7 @@ export class SearchFormComponent implements OnInit {
|
||||
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})$/;
|
||||
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59})$/;
|
||||
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
|
||||
regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
|
||||
regexBlockheight = /^[0-9]{1,9}$/;
|
||||
@ -33,7 +33,7 @@ export class SearchFormComponent implements OnInit {
|
||||
|
||||
@Output() searchTriggered = new EventEmitter();
|
||||
@ViewChild('searchResults') searchResults: SearchResultsComponent;
|
||||
@HostListener('keydown', ['$event']) keydown($event) {
|
||||
@HostListener('keydown', ['$event']) keydown($event): void {
|
||||
this.handleKeyDown($event);
|
||||
}
|
||||
|
||||
@ -47,7 +47,7 @@ export class SearchFormComponent implements OnInit {
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
ngOnInit(): void {
|
||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||
|
||||
this.searchForm = this.formBuilder.group({
|
||||
@ -61,70 +61,111 @@ export class SearchFormComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
this.typeAhead$ = this.searchForm.get('searchText').valueChanges
|
||||
.pipe(
|
||||
map((text) => {
|
||||
if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) {
|
||||
return text.substr(1);
|
||||
}
|
||||
return text.trim();
|
||||
}),
|
||||
debounceTime(200),
|
||||
distinctUntilChanged(),
|
||||
switchMap((text) => {
|
||||
if (!text.length) {
|
||||
return of([
|
||||
'',
|
||||
[],
|
||||
{
|
||||
nodes: [],
|
||||
channels: [],
|
||||
}
|
||||
]);
|
||||
}
|
||||
this.isTypeaheading$.next(true);
|
||||
if (!this.stateService.env.LIGHTNING) {
|
||||
return zip(
|
||||
of(text),
|
||||
this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))),
|
||||
[{ nodes: [], channels: [] }],
|
||||
of(this.regexBlockheight.test(text)),
|
||||
);
|
||||
}
|
||||
const searchText$ = this.searchForm.get('searchText').valueChanges
|
||||
.pipe(
|
||||
map((text) => {
|
||||
if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) {
|
||||
return text.substr(1);
|
||||
}
|
||||
return text.trim();
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
const searchResults$ = searchText$.pipe(
|
||||
debounceTime(200),
|
||||
switchMap((text) => {
|
||||
if (!text.length) {
|
||||
return of([
|
||||
[],
|
||||
{ nodes: [], channels: [] }
|
||||
]);
|
||||
}
|
||||
this.isTypeaheading$.next(true);
|
||||
if (!this.stateService.env.LIGHTNING) {
|
||||
return zip(
|
||||
of(text),
|
||||
this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))),
|
||||
this.apiService.lightningSearch$(text).pipe(catchError(() => of({
|
||||
[{ nodes: [], channels: [] }],
|
||||
);
|
||||
}
|
||||
return zip(
|
||||
this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))),
|
||||
this.apiService.lightningSearch$(text).pipe(catchError(() => of({
|
||||
nodes: [],
|
||||
channels: [],
|
||||
}))),
|
||||
);
|
||||
}),
|
||||
tap((result: any[]) => {
|
||||
this.isTypeaheading$.next(false);
|
||||
})
|
||||
);
|
||||
|
||||
this.typeAhead$ = combineLatest(
|
||||
[
|
||||
searchText$,
|
||||
searchResults$.pipe(
|
||||
startWith([
|
||||
[],
|
||||
{
|
||||
nodes: [],
|
||||
channels: [],
|
||||
}
|
||||
]))
|
||||
]
|
||||
).pipe(
|
||||
map((latestData) => {
|
||||
const searchText = latestData[0];
|
||||
if (!searchText.length) {
|
||||
return {
|
||||
searchText: '',
|
||||
hashQuickMatch: false,
|
||||
blockHeight: false,
|
||||
txId: false,
|
||||
address: false,
|
||||
addresses: [],
|
||||
nodes: [],
|
||||
channels: [],
|
||||
}))),
|
||||
);
|
||||
}),
|
||||
map((result: any[]) => {
|
||||
this.isTypeaheading$.next(false);
|
||||
if (this.network === 'bisq') {
|
||||
return result[0].map((address: string) => 'B' + address);
|
||||
};
|
||||
}
|
||||
|
||||
const result = latestData[1];
|
||||
const addressPrefixSearchResults = result[0];
|
||||
const lightningResults = result[1];
|
||||
|
||||
if (this.network === 'bisq') {
|
||||
return searchText.map((address: string) => 'B' + address);
|
||||
}
|
||||
|
||||
const matchesBlockHeight = this.regexBlockheight.test(searchText);
|
||||
const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText);
|
||||
const matchesBlockHash = this.regexBlockhash.test(searchText);
|
||||
const matchesAddress = this.regexAddress.test(searchText);
|
||||
|
||||
return {
|
||||
searchText: result[0],
|
||||
blockHeight: this.regexBlockheight.test(result[0]) ? [parseInt(result[0], 10)] : [],
|
||||
addresses: result[1],
|
||||
nodes: result[2].nodes,
|
||||
channels: result[2].channels,
|
||||
totalResults: result[1].length + result[2].nodes.length + result[2].channels.length,
|
||||
searchText: searchText,
|
||||
hashQuickMatch: +(matchesBlockHeight || matchesBlockHash || matchesTxId || matchesAddress),
|
||||
blockHeight: matchesBlockHeight,
|
||||
txId: matchesTxId,
|
||||
blockHash: matchesBlockHash,
|
||||
address: matchesAddress,
|
||||
addresses: addressPrefixSearchResults,
|
||||
nodes: lightningResults.nodes,
|
||||
channels: lightningResults.channels,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
handleKeyDown($event) {
|
||||
|
||||
handleKeyDown($event): void {
|
||||
this.searchResults.handleKeyDown($event);
|
||||
}
|
||||
|
||||
itemSelected() {
|
||||
itemSelected(): void {
|
||||
setTimeout(() => this.search());
|
||||
}
|
||||
|
||||
selectedResult(result: any) {
|
||||
selectedResult(result: any): void {
|
||||
if (typeof result === 'string') {
|
||||
this.search(result);
|
||||
} else if (typeof result === 'number') {
|
||||
@ -136,7 +177,7 @@ export class SearchFormComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
search(result?: string) {
|
||||
search(result?: string): void {
|
||||
const searchText = result || this.searchForm.value.searchText.trim();
|
||||
if (searchText) {
|
||||
this.isSearching = true;
|
||||
@ -170,7 +211,7 @@ export class SearchFormComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
navigate(url: string, searchText: string, extras?: any) {
|
||||
navigate(url: string, searchText: string, extras?: any): void {
|
||||
this.router.navigate([this.relativeUrlPipe.transform(url), searchText], extras);
|
||||
this.searchTriggered.emit();
|
||||
this.searchForm.setValue({
|
||||
|
@ -1,14 +1,32 @@
|
||||
<div class="dropdown-menu show" *ngIf="results" [hidden]="!results.blockHeight.length && !results.addresses.length && !results.nodes.length && !results.channels.length">
|
||||
<ng-template [ngIf]="results.blockHeight.length">
|
||||
<div class="dropdown-menu show" *ngIf="results" [hidden]="!results.hashQuickMatch && !results.addresses.length && !results.nodes.length && !results.channels.length">
|
||||
<ng-template [ngIf]="results.blockHeight">
|
||||
<div class="card-title">Bitcoin Block Height</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
Go to "{{ results.searchText }}"
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.txId">
|
||||
<div class="card-title">Bitcoin Transaction</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
Go to "{{ results.searchText | shortenString : 13 }}"
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.address">
|
||||
<div class="card-title">Bitcoin Address</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
Go to "{{ results.searchText | shortenString : isMobile ? 20 : 30 }}"
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.blockHash">
|
||||
<div class="card-title">Bitcoin Block</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
Go to "{{ results.searchText | shortenString : 13 }}"
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.addresses.length">
|
||||
<div class="card-title" *ngIf="stateService.env.LIGHTNING">Bitcoin Addresses</div>
|
||||
<div class="card-title">Bitcoin Addresses</div>
|
||||
<ng-template ngFor [ngForOf]="results.addresses" let-address let-i="index">
|
||||
<button (click)="clickItem(results.blockHeight.length + i)" [class.active]="(results.blockHeight.length + i) === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<button (click)="clickItem(results.hashQuickMatch + i)" [class.active]="(results.hashQuickMatch + i) === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ngb-highlight [result]="address | shortenString : isMobile ? 25 : 36" [term]="results.searchText"></ngb-highlight>
|
||||
</button>
|
||||
</ng-template>
|
||||
@ -16,7 +34,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.blockHeight.length + results.addresses.length + i)" [class.inactive]="node.status === 0" [class.active]="results.blockHeight.length + results.addresses.length + i === activeIdx" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]" type="button" role="option" class="dropdown-item">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.addresses.length + i)" [class.inactive]="node.status === 0" [class.active]="results.hashQuickMatch + 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]="results.searchText"></ngb-highlight> <span class="symbol">{{ node.public_key | shortenString : 10 }}</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
@ -24,7 +42,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.blockHeight.length + results.addresses.length + results.nodes.length + i)" [class.inactive]="channel.status === 2" [class.active]="results.blockHeight.length + results.addresses.length + results.nodes.length + i === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.addresses.length + results.nodes.length + i)" [class.inactive]="channel.status === 2" [class.active]="results.hashQuickMatch + results.addresses.length + results.nodes.length + i === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ngb-highlight [result]="channel.short_id" [term]="results.searchText"></ngb-highlight> <span class="symbol">{{ channel.id }}</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
|
@ -22,7 +22,7 @@ export class SearchResultsComponent implements OnChanges {
|
||||
ngOnChanges() {
|
||||
this.activeIdx = 0;
|
||||
if (this.results) {
|
||||
this.resultsFlattened = [...this.results.blockHeight, ...this.results.addresses, ...this.results.nodes, ...this.results.channels];
|
||||
this.resultsFlattened = [...(this.results.hashQuickMatch ? [this.results.searchText] : []), ...this.results.addresses, ...this.results.nodes, ...this.results.channels];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -190,7 +190,7 @@
|
||||
|
||||
<br>
|
||||
|
||||
<ng-container *ngIf="showFlow; else flowPlaceholder">
|
||||
<ng-container *ngIf="flowEnabled; else flowPlaceholder">
|
||||
<div class="title float-left">
|
||||
<h2 id="flow" i18n="transaction.flow|Transaction flow">Flow</h2>
|
||||
</div>
|
||||
@ -210,8 +210,6 @@
|
||||
[network]="network"
|
||||
[tooltip]="true"
|
||||
[inputIndex]="inputIndex" [outputIndex]="outputIndex"
|
||||
(selectInput)="selectInput($event)"
|
||||
(selectOutput)="selectOutput($event)"
|
||||
>
|
||||
</tx-bowtie-graph>
|
||||
</div>
|
||||
@ -238,7 +236,7 @@
|
||||
</div>
|
||||
|
||||
<div class="title-buttons">
|
||||
<button *ngIf="!showFlow" type="button" class="btn btn-outline-info flow-toggle btn-sm" (click)="toggleGraph()" i18n="show-diagram">Show diagram</button>
|
||||
<button *ngIf="!flowEnabled" type="button" class="btn btn-outline-info flow-toggle btn-sm" (click)="toggleGraph()" i18n="show-diagram">Show diagram</button>
|
||||
<button type="button" class="btn btn-outline-info btn-sm" (click)="txList.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -329,7 +327,7 @@
|
||||
|
||||
<br>
|
||||
|
||||
<ng-container *ngIf="showFlow">
|
||||
<ng-container *ngIf="flowEnabled">
|
||||
<div class="title">
|
||||
<h2 i18n="transaction.flow|Transaction flow">Flow</h2>
|
||||
</div>
|
||||
|
@ -18,6 +18,7 @@ import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { BlockExtended, CpfpInfo } from '../../interfaces/node-api.interface';
|
||||
import { LiquidUnblinding } from './liquid-ublinding';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-transaction',
|
||||
@ -40,6 +41,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
txReplacedSubscription: Subscription;
|
||||
blocksSubscription: Subscription;
|
||||
queryParamsSubscription: Subscription;
|
||||
urlFragmentSubscription: Subscription;
|
||||
fragmentParams: URLSearchParams;
|
||||
rbfTransaction: undefined | Transaction;
|
||||
cpfpInfo: CpfpInfo | null;
|
||||
showCpfpDetails = false;
|
||||
@ -49,12 +52,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
liquidUnblinding = new LiquidUnblinding();
|
||||
inputIndex: number;
|
||||
outputIndex: number;
|
||||
showFlow: boolean = true;
|
||||
graphExpanded: boolean = false;
|
||||
graphWidth: number = 1000;
|
||||
graphHeight: number = 360;
|
||||
inOutLimit: number = 150;
|
||||
maxInOut: number = 0;
|
||||
flowPrefSubscription: Subscription;
|
||||
hideFlow: boolean = this.stateService.hideFlow.value;
|
||||
overrideFlowPreference: boolean = null;
|
||||
flowEnabled: boolean;
|
||||
|
||||
tooltipPosition: { x: number, y: number };
|
||||
|
||||
@ -64,6 +70,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private stateService: StateService,
|
||||
private websocketService: WebsocketService,
|
||||
@ -78,12 +85,26 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
(network) => (this.network = network)
|
||||
);
|
||||
|
||||
this.setFlowEnabled();
|
||||
this.flowPrefSubscription = this.stateService.hideFlow.subscribe((hide) => {
|
||||
this.hideFlow = !!hide;
|
||||
this.setFlowEnabled();
|
||||
});
|
||||
|
||||
this.timeAvg$ = timer(0, 1000)
|
||||
.pipe(
|
||||
switchMap(() => this.stateService.difficultyAdjustment$),
|
||||
map((da) => da.timeAvg)
|
||||
);
|
||||
|
||||
this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
|
||||
this.fragmentParams = new URLSearchParams(fragment || '');
|
||||
const vin = parseInt(this.fragmentParams.get('vin'), 10);
|
||||
const vout = parseInt(this.fragmentParams.get('vout'), 10);
|
||||
this.inputIndex = (!isNaN(vin) && vin >= 0) ? vin : null;
|
||||
this.outputIndex = (!isNaN(vout) && vout >= 0) ? vout : null;
|
||||
});
|
||||
|
||||
this.fetchCpfpSubscription = this.fetchCpfp$
|
||||
.pipe(
|
||||
switchMap((txId) =>
|
||||
@ -123,13 +144,29 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
switchMap((params: ParamMap) => {
|
||||
const urlMatch = (params.get('id') || '').split(':');
|
||||
if (urlMatch.length === 2 && urlMatch[1].length === 64) {
|
||||
this.inputIndex = parseInt(urlMatch[0], 10);
|
||||
this.outputIndex = null;
|
||||
const vin = parseInt(urlMatch[0], 10);
|
||||
this.txId = urlMatch[1];
|
||||
// rewrite legacy vin syntax
|
||||
if (!isNaN(vin)) {
|
||||
this.fragmentParams.set('vin', vin.toString());
|
||||
this.fragmentParams.delete('vout');
|
||||
}
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.txId], {
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: this.fragmentParams.toString(),
|
||||
});
|
||||
} else {
|
||||
this.txId = urlMatch[0];
|
||||
this.outputIndex = urlMatch[1] === undefined ? null : parseInt(urlMatch[1], 10);
|
||||
this.inputIndex = null;
|
||||
const vout = parseInt(urlMatch[1], 10);
|
||||
if (urlMatch.length > 1 && !isNaN(vout)) {
|
||||
// rewrite legacy vout syntax
|
||||
this.fragmentParams.set('vout', vout.toString());
|
||||
this.fragmentParams.delete('vin');
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.txId], {
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: this.fragmentParams.toString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
this.seoService.setTitle(
|
||||
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
|
||||
@ -213,6 +250,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.fetchCpfp$.next(this.tx.txid);
|
||||
}
|
||||
}
|
||||
setTimeout(() => { this.applyFragment(); }, 0);
|
||||
},
|
||||
(error) => {
|
||||
this.error = error;
|
||||
@ -245,11 +283,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
|
||||
if (params.showFlow === 'false') {
|
||||
this.showFlow = false;
|
||||
this.overrideFlowPreference = false;
|
||||
} else if (params.showFlow === 'true') {
|
||||
this.overrideFlowPreference = true;
|
||||
} else {
|
||||
this.showFlow = true;
|
||||
this.setGraphSize();
|
||||
this.overrideFlowPreference = null;
|
||||
}
|
||||
this.setFlowEnabled();
|
||||
this.setGraphSize();
|
||||
});
|
||||
}
|
||||
|
||||
@ -325,15 +366,20 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
toggleGraph() {
|
||||
this.showFlow = !this.showFlow;
|
||||
const showFlow = !this.flowEnabled;
|
||||
this.stateService.hideFlow.next(!showFlow);
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { showFlow: this.showFlow },
|
||||
queryParams: { showFlow: showFlow },
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: 'flow'
|
||||
});
|
||||
}
|
||||
|
||||
setFlowEnabled() {
|
||||
this.flowEnabled = (this.overrideFlowPreference != null ? this.overrideFlowPreference : !this.hideFlow);
|
||||
}
|
||||
|
||||
expandGraph() {
|
||||
this.graphExpanded = true;
|
||||
}
|
||||
@ -342,14 +388,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.graphExpanded = false;
|
||||
}
|
||||
|
||||
selectInput(input) {
|
||||
this.inputIndex = input;
|
||||
this.outputIndex = null;
|
||||
}
|
||||
|
||||
selectOutput(output) {
|
||||
this.outputIndex = output;
|
||||
this.inputIndex = null;
|
||||
// simulate normal anchor fragment behavior
|
||||
applyFragment(): void {
|
||||
const anchor = Array.from(this.fragmentParams.entries()).find(([frag, value]) => value === '');
|
||||
if (anchor) {
|
||||
const anchorElement = document.getElementById(anchor[0]);
|
||||
if (anchorElement) {
|
||||
anchorElement.scrollIntoView();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
@ -365,6 +412,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.txReplacedSubscription.unsubscribe();
|
||||
this.blocksSubscription.unsubscribe();
|
||||
this.queryParamsSubscription.unsubscribe();
|
||||
this.flowPrefSubscription.unsubscribe();
|
||||
this.urlFragmentSubscription.unsubscribe();
|
||||
this.leaveTransaction();
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<ng-template #defaultPrevout>
|
||||
<a [routerLink]="['/tx/' | relativeUrl, vin.txid + ':' + vin.vout]" class="red">
|
||||
<a [routerLink]="['/tx/' | relativeUrl, vin.txid]" [fragment]="'vout=' + vin.vout" class="red">
|
||||
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
||||
</a>
|
||||
</ng-template>
|
||||
@ -220,7 +220,7 @@
|
||||
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
||||
</span>
|
||||
<ng-template #spent>
|
||||
<a *ngIf="tx._outspends[vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, tx._outspends[vindex].vin + ':' + tx._outspends[vindex].txid]" class="red">
|
||||
<a *ngIf="tx._outspends[vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, tx._outspends[vindex].txid]" [fragment]="'vin=' + tx._outspends[vindex].vin" class="red">
|
||||
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
||||
</a>
|
||||
<ng-template #outputNoTxId>
|
||||
|
@ -68,7 +68,7 @@
|
||||
<path
|
||||
[attr.d]="input.path"
|
||||
class="line {{input.class}}"
|
||||
[class.highlight]="inputData[i].index === inputIndex"
|
||||
[class.highlight]="inputIndex != null && inputData[i].index === inputIndex"
|
||||
[style]="input.style"
|
||||
attr.marker-start="url(#{{input.class}}-arrow)"
|
||||
(pointerover)="onHover($event, 'input', i);"
|
||||
@ -80,7 +80,7 @@
|
||||
<path
|
||||
[attr.d]="output.path"
|
||||
class="line {{output.class}}"
|
||||
[class.highlight]="outputData[i].index === outputIndex"
|
||||
[class.highlight]="outputIndex != null && outputData[i].index === outputIndex"
|
||||
[style]="output.style"
|
||||
attr.marker-start="url(#{{output.class}}-arrow)"
|
||||
(pointerover)="onHover($event, 'output', i);"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, HostListener } from '@angular/core';
|
||||
import { Component, OnInit, Input, OnChanges, HostListener } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Outspend, Transaction } from '../../interfaces/electrs.interface';
|
||||
import { Router } from '@angular/router';
|
||||
@ -43,9 +43,6 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
@Input() inputIndex: number;
|
||||
@Input() outputIndex: number;
|
||||
|
||||
@Output() selectInput = new EventEmitter<number>();
|
||||
@Output() selectOutput = new EventEmitter<number>();
|
||||
|
||||
inputData: Xput[];
|
||||
outputData: Xput[];
|
||||
inputs: SvgLine[];
|
||||
@ -368,24 +365,42 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
onClick(event, side, index): void {
|
||||
if (side === 'input') {
|
||||
const input = this.tx.vin[index];
|
||||
if (input && input.txid && input.vout != null) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/tx'), input.txid + ':' + input.vout], {
|
||||
if (input && !input.is_coinbase && !input.is_pegin && input.txid && input.vout != null) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/tx'), input.txid], {
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: 'flow'
|
||||
fragment: (new URLSearchParams({
|
||||
flow: '',
|
||||
vout: input.vout.toString(),
|
||||
})).toString(),
|
||||
});
|
||||
} else if (index != null) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.tx.txid], {
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: (new URLSearchParams({
|
||||
flow: '',
|
||||
vin: index.toString(),
|
||||
})).toString(),
|
||||
});
|
||||
} else {
|
||||
this.selectInput.emit(index);
|
||||
}
|
||||
} else {
|
||||
const output = this.tx.vout[index];
|
||||
const outspend = this.outspends[index];
|
||||
if (output && outspend && outspend.spent && outspend.txid) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/tx'), outspend.vin + ':' + outspend.txid], {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/tx'), outspend.txid], {
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: 'flow'
|
||||
fragment: (new URLSearchParams({
|
||||
flow: '',
|
||||
vin: outspend.vin.toString(),
|
||||
})).toString(),
|
||||
});
|
||||
} else if (index != null) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.tx.txid], {
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: (new URLSearchParams({
|
||||
flow: '',
|
||||
vout: index.toString(),
|
||||
})).toString(),
|
||||
});
|
||||
} else {
|
||||
this.selectOutput.emit(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,10 @@
|
||||
<ng-template #notFullyTaproot>
|
||||
<span *ngIf="segwitGains.realizedTaprootGains && segwitGains.potentialTaprootGains; else noTaproot" class="badge badge-warning mr-1" i18n-ngbTooltip="Tooltip about fees that saved and could be saved with taproot" ngbTooltip="This transaction uses Taproot and already saved at least {{ segwitGains.realizedTaprootGains * 100 | number: '1.0-0' }}% on fees, but could save an additional {{ segwitGains.potentialTaprootGains * 100 | number: '1.0-0' }}% by fully using Taproot" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
|
||||
<ng-template #noTaproot>
|
||||
<span *ngIf="segwitGains.potentialTaprootGains; else taprootButNoGains" class="badge badge-danger mr-1" i18n-ngbTooltip="Tooltip about fees that could be saved with taproot" ngbTooltip="This transaction could save {{ segwitGains.potentialTaprootGains * 100 | number: '1.0-0' }}% on fees by using Taproot" placement="bottom"><del i18n="tx-features.tag.taproot|Taproot">Taproot</del></span>
|
||||
<span *ngIf="segwitGains.potentialTaprootGains && segwitGains.potentialTaprootGains > 0; else negativeTaprootGains" class="badge badge-danger mr-1" i18n-ngbTooltip="Tooltip about fees that could be saved with taproot" ngbTooltip="This transaction could save {{ segwitGains.potentialTaprootGains * 100 | number: '1.0-0' }}% on fees by using Taproot" placement="bottom"><del i18n="tx-features.tag.taproot|Taproot">Taproot</del></span>
|
||||
<ng-template #negativeTaprootGains>
|
||||
<span *ngIf="!isTaproot; else taprootButNoGains" class="badge badge-danger mr-1" i18n-ngbTooltip="Tooltip about using taproot" ngbTooltip="This transaction does not use Taproot" placement="bottom"><del i18n="tx-features.tag.taproot|Taproot">Taproot</del></span>
|
||||
</ng-template>
|
||||
<ng-template #taprootButNoGains>
|
||||
<span *ngIf="isTaproot" class="badge badge-success mr-1" i18n-ngbTooltip="Tooltip about taproot" ngbTooltip="This transaction uses Taproot" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
|
||||
</ng-template>
|
||||
|
@ -39,6 +39,7 @@ if (browserWindowEnv.BASE_MODULE && (browserWindowEnv.BASE_MODULE === 'bisq' ||
|
||||
},
|
||||
{
|
||||
path: 'faq',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: DocsComponent
|
||||
},
|
||||
{
|
||||
|
@ -37,10 +37,12 @@ const routes: Routes = [
|
||||
children: [
|
||||
{
|
||||
path: 'mining/pool/:slug',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: PoolComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
@ -51,6 +53,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'mempool-block/:id',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
@ -61,62 +64,77 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'graphs',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: GraphsComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'mempool',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: StatisticsComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/hashrate-difficulty',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: HashrateChartComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/pools-dominance',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: HashrateChartPoolsComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/pools',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: PoolRankingComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/block-fees',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: BlockFeesGraphComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/block-rewards',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: BlockRewardsGraphComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/block-fee-rates',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: BlockFeeRatesGraphComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/block-sizes-weights',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: BlockSizesWeightsGraphComponent,
|
||||
},
|
||||
{
|
||||
path: 'lightning/nodes-networks',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: NodesNetworksChartComponent,
|
||||
},
|
||||
{
|
||||
path: 'lightning/capacity',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: LightningStatisticsChartComponent,
|
||||
},
|
||||
{
|
||||
path: 'lightning/nodes-per-isp',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: NodesPerISPChartComponent,
|
||||
},
|
||||
{
|
||||
path: 'lightning/nodes-per-country',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: NodesPerCountryChartComponent,
|
||||
},
|
||||
{
|
||||
path: 'lightning/nodes-map',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: NodesMap,
|
||||
},
|
||||
{
|
||||
path: 'lightning/nodes-channels-map',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: NodesChannelsMap,
|
||||
},
|
||||
{
|
||||
@ -125,6 +143,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'mining/block-prediction',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: BlockPredictionGraphComponent,
|
||||
},
|
||||
]
|
||||
@ -141,6 +160,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'tv',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: TelevisionComponent
|
||||
},
|
||||
];
|
||||
|
@ -53,6 +53,10 @@ export class LightningApiService {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/statistics');
|
||||
}
|
||||
|
||||
getNodeFeeHistogram$(publicKey: string): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/fees/histogram');
|
||||
}
|
||||
|
||||
getNodesRanking$(): Observable<INodesRanking> {
|
||||
return this.httpClient.get<INodesRanking>(this.apiBasePath + '/api/v1/lightning/nodes/rankings');
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import { ChannelBoxComponent } from './channel/channel-box/channel-box.component
|
||||
import { ClosingTypeComponent } from './channel/closing-type/closing-type.component';
|
||||
import { LightningStatisticsChartComponent } from './statistics-chart/lightning-statistics-chart.component';
|
||||
import { NodeStatisticsChartComponent } from './node-statistics-chart/node-statistics-chart.component';
|
||||
import { NodeFeeChartComponent } from './node-fee-chart/node-fee-chart.component';
|
||||
import { GraphsModule } from '../graphs/graphs.module';
|
||||
import { NodesNetworksChartComponent } from './nodes-networks-chart/nodes-networks-chart.component';
|
||||
import { ChannelsStatisticsComponent } from './channels-statistics/channels-statistics.component';
|
||||
@ -38,6 +39,7 @@ import { GroupComponent } from './group/group.component';
|
||||
NodesListComponent,
|
||||
NodeStatisticsComponent,
|
||||
NodeStatisticsChartComponent,
|
||||
NodeFeeChartComponent,
|
||||
NodeComponent,
|
||||
ChannelsListComponent,
|
||||
ChannelComponent,
|
||||
@ -73,6 +75,7 @@ import { GroupComponent } from './group/group.component';
|
||||
NodesListComponent,
|
||||
NodeStatisticsComponent,
|
||||
NodeStatisticsChartComponent,
|
||||
NodeFeeChartComponent,
|
||||
NodeComponent,
|
||||
ChannelsListComponent,
|
||||
ChannelComponent,
|
||||
|
@ -21,10 +21,12 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'node/:public_key',
|
||||
data: { networkSpecific: true },
|
||||
component: NodeComponent,
|
||||
},
|
||||
{
|
||||
path: 'channel/:short_id',
|
||||
data: { networkSpecific: true },
|
||||
component: ChannelComponent,
|
||||
},
|
||||
{
|
||||
|
@ -0,0 +1,7 @@
|
||||
<div class="full-container">
|
||||
<h2 i18n="lightning.node-fee-distribution">Fee distribution</h2>
|
||||
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>d
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,5 @@
|
||||
.full-container {
|
||||
margin-top: 25px;
|
||||
margin-bottom: 25px;
|
||||
min-height: 100%;
|
||||
}
|
@ -0,0 +1,265 @@
|
||||
import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { download } from '../../shared/graphs.utils';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-node-fee-chart',
|
||||
templateUrl: './node-fee-chart.component.html',
|
||||
styleUrls: ['./node-fee-chart.component.scss'],
|
||||
styles: [`
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
})
|
||||
export class NodeFeeChartComponent implements OnInit {
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
};
|
||||
|
||||
@HostBinding('attr.dir') dir = 'ltr';
|
||||
|
||||
isLoading = true;
|
||||
chartInstance: any = undefined;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private lightningApiService: LightningApiService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private amountShortenerPipe: AmountShortenerPipe,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.activatedRoute.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.isLoading = true;
|
||||
return this.lightningApiService.getNodeFeeHistogram$(params.get('public_key'));
|
||||
}),
|
||||
).subscribe((data) => {
|
||||
if (data && data.incoming && data.outgoing) {
|
||||
const outgoingHistogram = this.bucketsToHistogram(data.outgoing);
|
||||
const incomingHistogram = this.bucketsToHistogram(data.incoming);
|
||||
this.prepareChartOptions(outgoingHistogram, incomingHistogram);
|
||||
}
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
bucketsToHistogram(buckets): { label: string, count: number, capacity: number}[] {
|
||||
const histogram = [];
|
||||
let increment = 1;
|
||||
let lower = -increment;
|
||||
let upper = 0;
|
||||
|
||||
let nullBucket;
|
||||
if (buckets.length && buckets[0] && buckets[0].bucket == null) {
|
||||
nullBucket = buckets.shift();
|
||||
}
|
||||
|
||||
while (upper <= 5000) {
|
||||
let bucket;
|
||||
if (buckets.length && buckets[0] && upper >= Number(buckets[0].bucket)) {
|
||||
bucket = buckets.shift();
|
||||
}
|
||||
histogram.push({
|
||||
label: upper === 0 ? '0 ppm' : `${lower} - ${upper} ppm`,
|
||||
count: Number(bucket?.count || 0) + (upper === 0 ? Number(nullBucket?.count || 0) : 0),
|
||||
capacity: Number(bucket?.capacity || 0) + (upper === 0 ? Number(nullBucket?.capacity || 0) : 0),
|
||||
});
|
||||
|
||||
if (upper >= increment * 10) {
|
||||
increment *= 10;
|
||||
lower = increment;
|
||||
upper = increment + increment;
|
||||
} else {
|
||||
lower += increment;
|
||||
upper += increment;
|
||||
}
|
||||
}
|
||||
const rest = buckets.reduce((acc, bucket) => {
|
||||
acc.count += Number(bucket.count);
|
||||
acc.capacity += Number(bucket.capacity);
|
||||
return acc;
|
||||
}, { count: 0, capacity: 0 });
|
||||
histogram.push({
|
||||
label: `5000+ ppm`,
|
||||
count: rest.count,
|
||||
capacity: rest.capacity,
|
||||
});
|
||||
return histogram;
|
||||
}
|
||||
|
||||
prepareChartOptions(outgoingData, incomingData): void {
|
||||
let title: object;
|
||||
if (outgoingData.length === 0) {
|
||||
title = {
|
||||
textStyle: {
|
||||
color: 'grey',
|
||||
fontSize: 15
|
||||
},
|
||||
text: $localize`No data to display yet. Try again later.`,
|
||||
left: 'center',
|
||||
top: 'center'
|
||||
};
|
||||
}
|
||||
|
||||
this.chartOptions = {
|
||||
title: outgoingData.length === 0 ? title : undefined,
|
||||
animation: false,
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
left: 65,
|
||||
},
|
||||
tooltip: {
|
||||
show: !this.isMobile(),
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'line'
|
||||
},
|
||||
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||
borderRadius: 4,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
textStyle: {
|
||||
color: '#b1b1b1',
|
||||
align: 'left',
|
||||
},
|
||||
borderColor: '#000',
|
||||
formatter: (ticks): string => {
|
||||
return `
|
||||
<b style="color: white; margin-left: 2px">${ticks[0].data.label}</b><br>
|
||||
<br>
|
||||
<b style="color: white; margin-left: 2px">${ticks[0].marker} Outgoing</b><br>
|
||||
<span>Capacity: ${this.amountShortenerPipe.transform(ticks[0].data.capacity, 2, undefined, true)} sats</span><br>
|
||||
<span>Channels: ${ticks[0].data.count}</span><br>
|
||||
<br>
|
||||
<b style="color: white; margin-left: 2px">${ticks[1].marker} Incoming</b><br>
|
||||
<span>Capacity: ${this.amountShortenerPipe.transform(ticks[1].data.capacity, 2, undefined, true)} sats</span><br>
|
||||
<span>Channels: ${ticks[1].data.count}</span><br>
|
||||
`;
|
||||
}
|
||||
},
|
||||
xAxis: outgoingData.length === 0 ? undefined : {
|
||||
type: 'category',
|
||||
axisLine: { onZero: true },
|
||||
axisLabel: {
|
||||
align: 'center',
|
||||
fontSize: 11,
|
||||
lineHeight: 12,
|
||||
hideOverlap: true,
|
||||
padding: [0, 5],
|
||||
},
|
||||
data: outgoingData.map(bucket => bucket.label)
|
||||
},
|
||||
legend: outgoingData.length === 0 ? undefined : {
|
||||
padding: 10,
|
||||
data: [
|
||||
{
|
||||
name: 'Outgoing Fees',
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
},
|
||||
icon: 'roundRect',
|
||||
},
|
||||
{
|
||||
name: 'Incoming Fees',
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
},
|
||||
icon: 'roundRect',
|
||||
},
|
||||
],
|
||||
},
|
||||
yAxis: outgoingData.length === 0 ? undefined : [
|
||||
{
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: 'rgb(110, 112, 121)',
|
||||
formatter: (val) => {
|
||||
return `${this.amountShortenerPipe.transform(Math.abs(val), 2, undefined, true)} sats`;
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dotted',
|
||||
color: '#ffffff66',
|
||||
opacity: 0.25,
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
series: outgoingData.length === 0 ? undefined : [
|
||||
{
|
||||
zlevel: 0,
|
||||
name: 'Outgoing Fees',
|
||||
data: outgoingData.map(bucket => ({
|
||||
value: bucket.capacity,
|
||||
label: bucket.label,
|
||||
capacity: bucket.capacity,
|
||||
count: bucket.count,
|
||||
})),
|
||||
type: 'bar',
|
||||
barWidth: '90%',
|
||||
barMaxWidth: 50,
|
||||
stack: 'fees',
|
||||
},
|
||||
{
|
||||
zlevel: 0,
|
||||
name: 'Incoming Fees',
|
||||
data: incomingData.map(bucket => ({
|
||||
value: -bucket.capacity,
|
||||
label: bucket.label,
|
||||
capacity: bucket.capacity,
|
||||
count: bucket.count,
|
||||
})),
|
||||
type: 'bar',
|
||||
barWidth: '90%',
|
||||
barMaxWidth: 50,
|
||||
stack: 'fees',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
onChartInit(ec) {
|
||||
if (this.chartInstance !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chartInstance = ec;
|
||||
}
|
||||
|
||||
isMobile() {
|
||||
return (window.innerWidth <= 767.98);
|
||||
}
|
||||
|
||||
onSaveChart() {
|
||||
// @ts-ignore
|
||||
const prevBottom = this.chartOptions.grid.bottom;
|
||||
// @ts-ignore
|
||||
this.chartOptions.grid.bottom = 40;
|
||||
this.chartOptions.backgroundColor = '#11131f';
|
||||
this.chartInstance.setOption(this.chartOptions);
|
||||
download(this.chartInstance.getDataURL({
|
||||
pixelRatio: 2,
|
||||
}), `node-fee-chart.svg`);
|
||||
// @ts-ignore
|
||||
this.chartOptions.grid.bottom = prevBottom;
|
||||
this.chartOptions.backgroundColor = 'none';
|
||||
this.chartInstance.setOption(this.chartOptions);
|
||||
}
|
||||
}
|
@ -140,6 +140,8 @@
|
||||
|
||||
<app-node-channels style="display:block;margin-bottom: 40px" [publicKey]="node.public_key"></app-node-channels>
|
||||
|
||||
<app-node-fee-chart style="display:block;margin-bottom: 40px"></app-node-fee-chart>
|
||||
|
||||
<div class="d-flex">
|
||||
<h2 *ngIf="channelsListStatus === 'open'">
|
||||
<span i18n="lightning.open-channels">Open channels</span>
|
||||
|
90
frontend/src/app/services/navigation.service.ts
Normal file
90
frontend/src/app/services/navigation.service.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Router, ActivatedRoute, NavigationEnd, ActivatedRouteSnapshot } from '@angular/router';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { StateService } from './state.service';
|
||||
|
||||
const networkModules = {
|
||||
bitcoin: {
|
||||
subnets: [
|
||||
{ name: 'mainnet', path: '' },
|
||||
{ name: 'testnet', path: '/testnet' },
|
||||
{ name: 'signet', path: '/signet' },
|
||||
],
|
||||
},
|
||||
liquid: {
|
||||
subnets: [
|
||||
{ name: 'liquid', path: '' },
|
||||
{ name: 'liquidtestnet', path: '/testnet' },
|
||||
],
|
||||
},
|
||||
bisq: {
|
||||
subnets: [
|
||||
{ name: 'bisq', path: '' },
|
||||
],
|
||||
},
|
||||
};
|
||||
const networks = Object.keys(networkModules);
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class NavigationService {
|
||||
subnetPaths = new BehaviorSubject<Record<string,string>>({});
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private router: Router,
|
||||
) {
|
||||
this.router.events.pipe(
|
||||
filter(event => event instanceof NavigationEnd),
|
||||
map(() => this.router.routerState.snapshot.root),
|
||||
).subscribe((state) => {
|
||||
this.updateSubnetPaths(state);
|
||||
});
|
||||
}
|
||||
|
||||
// For each network (bitcoin/liquid/bisq), find and save the longest url path compatible with the current route
|
||||
updateSubnetPaths(root: ActivatedRouteSnapshot): void {
|
||||
let path = '';
|
||||
const networkPaths = {};
|
||||
let route = root;
|
||||
// traverse the router state tree until all network paths are set, or we reach the end of the tree
|
||||
while (!networks.reduce((acc, network) => acc && !!networkPaths[network], true) && route) {
|
||||
// 'networkSpecific' paths may correspond to valid routes on other networks, but aren't directly compatible
|
||||
// (e.g. we shouldn't link a mainnet transaction page to the same txid on testnet or liquid)
|
||||
if (route.data?.networkSpecific) {
|
||||
networks.forEach(network => {
|
||||
if (networkPaths[network] == null) {
|
||||
networkPaths[network] = path;
|
||||
}
|
||||
});
|
||||
}
|
||||
// null or empty networks list is shorthand for "compatible with every network"
|
||||
if (route.data?.networks?.length) {
|
||||
// if the list is non-empty, only those networks are compatible
|
||||
networks.forEach(network => {
|
||||
if (!route.data.networks.includes(network)) {
|
||||
if (networkPaths[network] == null) {
|
||||
networkPaths[network] = path;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (route.url?.length) {
|
||||
path = [path, ...route.url.map(segment => segment.path).filter(path => {
|
||||
return path.length && !['testnet', 'signet'].includes(path);
|
||||
})].join('/');
|
||||
}
|
||||
route = route.firstChild;
|
||||
}
|
||||
|
||||
const subnetPaths = {};
|
||||
Object.entries(networkModules).forEach(([key, network]) => {
|
||||
network.subnets.forEach(subnet => {
|
||||
subnetPaths[subnet.name] = subnet.path + (networkPaths[key] != null ? networkPaths[key] : path);
|
||||
});
|
||||
});
|
||||
this.subnetPaths.next(subnetPaths);
|
||||
}
|
||||
}
|
@ -110,6 +110,7 @@ export class StateService {
|
||||
|
||||
blockScrolling$: Subject<boolean> = new Subject<boolean>();
|
||||
timeLtr: BehaviorSubject<boolean>;
|
||||
hideFlow: BehaviorSubject<boolean>;
|
||||
|
||||
constructor(
|
||||
@Inject(PLATFORM_ID) private platformId: any,
|
||||
@ -159,6 +160,16 @@ export class StateService {
|
||||
this.timeLtr.subscribe((ltr) => {
|
||||
this.storageService.setValue('time-preference-ltr', ltr ? 'true' : 'false');
|
||||
});
|
||||
|
||||
const savedFlowPreference = this.storageService.getValue('flow-preference');
|
||||
this.hideFlow = new BehaviorSubject<boolean>(savedFlowPreference === 'hide');
|
||||
this.hideFlow.subscribe((hide) => {
|
||||
if (hide) {
|
||||
this.storageService.setValue('flow-preference', hide ? 'hide' : 'show');
|
||||
} else {
|
||||
this.storageService.removeItem('flow-preference');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setNetworkBasedonUrl(url: string) {
|
||||
@ -170,7 +181,8 @@ export class StateService {
|
||||
// (?:[a-z]{2}(?:-[A-Z]{2})?\/)? optional locale prefix (non-capturing)
|
||||
// (?:preview\/)? optional "preview" prefix (non-capturing)
|
||||
// (bisq|testnet|liquidtestnet|liquid|signet)/ network string (captured as networkMatches[1])
|
||||
const networkMatches = url.match(/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?(?:preview\/)?(bisq|testnet|liquidtestnet|liquid|signet)/);
|
||||
// ($|\/) network string must end or end with a slash
|
||||
const networkMatches = url.match(/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?(?:preview\/)?(bisq|testnet|liquidtestnet|liquid|signet)($|\/)/);
|
||||
switch (networkMatches && networkMatches[1]) {
|
||||
case 'liquid':
|
||||
if (this.network !== 'liquid') {
|
||||
|
@ -46,4 +46,12 @@ export class StorageService {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
removeItem(key: string): void {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
6180
frontend/src/locale/messages.lt.xlf
Normal file
6180
frontend/src/locale/messages.lt.xlf
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -16,7 +16,8 @@
|
||||
],
|
||||
"lib": [
|
||||
"es2018",
|
||||
"dom"
|
||||
"dom",
|
||||
"dom.iterable"
|
||||
]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
|
Loading…
Reference in New Issue
Block a user