Merge branch 'master' into frontend_runtime_config

This commit is contained in:
Felipe Knorr Kuhn 2022-10-22 10:28:35 -07:00 committed by GitHub
commit 847aa1ba13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 77416 additions and 16432 deletions

View File

@ -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');
}
}
/**

View File

@ -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`;

View File

@ -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);

View File

@ -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;

View File

@ -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) {

View File

@ -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

View File

@ -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
},
{

View File

@ -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,
},
{

View File

@ -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>

View File

@ -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 {

View File

@ -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>

View File

@ -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 {

View File

@ -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>

View File

@ -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 {

View File

@ -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({

View File

@ -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> &nbsp;<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> &nbsp;<span class="symbol">{{ channel.id }}</span>
</button>
</ng-template>

View File

@ -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];
}
}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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);"

View File

@ -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);
}
}
}

View File

@ -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>

View File

@ -39,6 +39,7 @@ if (browserWindowEnv.BASE_MODULE && (browserWindowEnv.BASE_MODULE === 'bisq' ||
},
{
path: 'faq',
data: { networks: ['bitcoin'] },
component: DocsComponent
},
{

View File

@ -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
},
];

View File

@ -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');
}

View File

@ -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,

View File

@ -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,
},
{

View File

@ -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>

View File

@ -0,0 +1,5 @@
.full-container {
margin-top: 25px;
margin-bottom: 25px;
min-height: 100%;
}

View File

@ -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);
}
}

View File

@ -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>

View 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);
}
}

View File

@ -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') {

View File

@ -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

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

View File

@ -16,7 +16,8 @@
],
"lib": [
"es2018",
"dom"
"dom",
"dom.iterable"
]
},
"angularCompilerOptions": {