Merge branch 'master' into hunicus/enterprise-cta-docs
This commit is contained in:
@@ -1,28 +1,11 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { AppPreloadingStrategy } from './app.preloading-strategy'
|
||||
import { StartComponent } from './components/start/start.component';
|
||||
import { TransactionComponent } from './components/transaction/transaction.component';
|
||||
import { BlockComponent } from './components/block/block.component';
|
||||
import { BlockViewComponent } from './components/block-view/block-view.component';
|
||||
import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component';
|
||||
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
|
||||
import { ClockComponent } from './components/clock/clock.component';
|
||||
import { AddressComponent } from './components/address/address.component';
|
||||
import { MasterPageComponent } from './components/master-page/master-page.component';
|
||||
import { AboutComponent } from './components/about/about.component';
|
||||
import { StatusViewComponent } from './components/status-view/status-view.component';
|
||||
import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component';
|
||||
import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component';
|
||||
import { TrademarkPolicyComponent } from './components/trademark-policy/trademark-policy.component';
|
||||
import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
|
||||
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
|
||||
import { BlocksList } from './components/blocks-list/blocks-list.component';
|
||||
import { RbfList } from './components/rbf-list/rbf-list.component';
|
||||
import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component';
|
||||
import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component';
|
||||
import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component';
|
||||
import { AssetsComponent } from './components/assets/assets.component';
|
||||
import { AssetComponent } from './components/asset/asset.component';
|
||||
import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component';
|
||||
import { CalculatorComponent } from './components/calculator/calculator.component';
|
||||
|
||||
const browserWindow = window || {};
|
||||
// @ts-ignore
|
||||
@@ -35,95 +18,13 @@ let routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule),
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: MasterPageComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'mining/blocks',
|
||||
redirectTo: 'blocks',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'tx/push',
|
||||
component: PushTransactionComponent,
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
component: AboutComponent,
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
component: BlocksList,
|
||||
},
|
||||
{
|
||||
path: 'rbf',
|
||||
component: RbfList,
|
||||
},
|
||||
{
|
||||
path: 'terms-of-service',
|
||||
component: TermsOfServiceComponent
|
||||
},
|
||||
{
|
||||
path: 'privacy-policy',
|
||||
component: PrivacyPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'trademark-policy',
|
||||
component: TrademarkPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
children: [],
|
||||
component: AddressComponent,
|
||||
data: {
|
||||
ogImage: true,
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
component: StartComponent,
|
||||
data: { networkSpecific: true },
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: TransactionComponent
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'block',
|
||||
component: StartComponent,
|
||||
data: { networkSpecific: true },
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: BlockComponent,
|
||||
data: {
|
||||
ogImage: true
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'docs',
|
||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
{
|
||||
path: 'lightning',
|
||||
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule),
|
||||
data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true, networks: ['bitcoin'] },
|
||||
},
|
||||
],
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
@@ -132,7 +33,8 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
@@ -151,88 +53,13 @@ let routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: MasterPageComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'tx/push',
|
||||
component: PushTransactionComponent,
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
component: AboutComponent,
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
component: BlocksList,
|
||||
},
|
||||
{
|
||||
path: 'rbf',
|
||||
component: RbfList,
|
||||
},
|
||||
{
|
||||
path: 'terms-of-service',
|
||||
component: TermsOfServiceComponent
|
||||
},
|
||||
{
|
||||
path: 'privacy-policy',
|
||||
component: PrivacyPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'trademark-policy',
|
||||
component: TrademarkPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
children: [],
|
||||
component: AddressComponent,
|
||||
data: {
|
||||
ogImage: true,
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: TransactionComponent
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'block',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: BlockComponent,
|
||||
data: {
|
||||
ogImage: true
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'docs',
|
||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
{
|
||||
path: 'lightning',
|
||||
data: { networks: ['bitcoin'] },
|
||||
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
|
||||
},
|
||||
],
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
@@ -241,7 +68,8 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
@@ -252,97 +80,13 @@ let routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: MasterPageComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'mining/blocks',
|
||||
redirectTo: 'blocks',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'tx/push',
|
||||
component: PushTransactionComponent,
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
component: AboutComponent,
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
component: BlocksList,
|
||||
},
|
||||
{
|
||||
path: 'rbf',
|
||||
component: RbfList,
|
||||
},
|
||||
{
|
||||
path: 'tools/calculator',
|
||||
component: CalculatorComponent
|
||||
},
|
||||
{
|
||||
path: 'terms-of-service',
|
||||
component: TermsOfServiceComponent
|
||||
},
|
||||
{
|
||||
path: 'privacy-policy',
|
||||
component: PrivacyPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'trademark-policy',
|
||||
component: TrademarkPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
children: [],
|
||||
component: AddressComponent,
|
||||
data: {
|
||||
ogImage: true,
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: TransactionComponent
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'block',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: BlockComponent,
|
||||
data: {
|
||||
ogImage: true
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'docs',
|
||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
{
|
||||
path: 'lightning',
|
||||
data: { networks: ['bitcoin'] },
|
||||
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
|
||||
},
|
||||
],
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'preview',
|
||||
@@ -373,6 +117,18 @@ let routes: Routes = [
|
||||
path: 'clock/:mode/:index',
|
||||
component: ClockComponent,
|
||||
},
|
||||
{
|
||||
path: 'view/block/:id',
|
||||
component: BlockViewComponent,
|
||||
},
|
||||
{
|
||||
path: 'view/mempool-block/:index',
|
||||
component: MempoolBlockViewComponent,
|
||||
},
|
||||
{
|
||||
path: 'view/blocks',
|
||||
component: EightBlocksComponent,
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
@@ -380,7 +136,8 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
@@ -391,7 +148,6 @@ let routes: Routes = [
|
||||
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'bisq') {
|
||||
routes = [{
|
||||
path: '',
|
||||
component: BisqMasterPageComponent,
|
||||
loadChildren: () => import('./bisq/bisq.module').then(m => m.BisqModule)
|
||||
}];
|
||||
}
|
||||
@@ -404,105 +160,13 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
|
||||
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: LiquidMasterPageComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'tx/push',
|
||||
component: PushTransactionComponent,
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
component: AboutComponent,
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
component: BlocksList,
|
||||
},
|
||||
{
|
||||
path: 'terms-of-service',
|
||||
component: TermsOfServiceComponent
|
||||
},
|
||||
{
|
||||
path: 'privacy-policy',
|
||||
component: PrivacyPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'trademark-policy',
|
||||
component: TrademarkPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
children: [],
|
||||
component: AddressComponent,
|
||||
data: {
|
||||
ogImage: true,
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: TransactionComponent
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'block',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: BlockComponent,
|
||||
data: {
|
||||
ogImage: true
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: 'all'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'docs',
|
||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
],
|
||||
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
@@ -511,7 +175,8 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
|
||||
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
@@ -522,110 +187,13 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
|
||||
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: LiquidMasterPageComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'tx/push',
|
||||
component: PushTransactionComponent,
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
component: AboutComponent,
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
component: BlocksList,
|
||||
},
|
||||
{
|
||||
path: 'terms-of-service',
|
||||
component: TermsOfServiceComponent
|
||||
},
|
||||
{
|
||||
path: 'privacy-policy',
|
||||
component: PrivacyPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'trademark-policy',
|
||||
component: TrademarkPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
children: [],
|
||||
component: AddressComponent,
|
||||
data: {
|
||||
ogImage: true,
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: TransactionComponent
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'block',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: BlockComponent,
|
||||
data: {
|
||||
ogImage: true
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: 'featured'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'docs',
|
||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
],
|
||||
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'preview',
|
||||
@@ -647,7 +215,8 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
|
||||
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
|
||||
@@ -22,6 +22,7 @@ import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
|
||||
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
|
||||
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
|
||||
import { AppPreloadingStrategy } from './app.preloading-strategy';
|
||||
import { ServicesApiServices } from './services/services-api.service';
|
||||
|
||||
const providers = [
|
||||
ElectrsApiService,
|
||||
@@ -40,6 +41,7 @@ const providers = [
|
||||
FiatCurrencyPipe,
|
||||
CapAddressPipe,
|
||||
AppPreloadingStrategy,
|
||||
ServicesApiServices,
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
|
||||
];
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
|
||||
this.location.replaceState(
|
||||
this.router.createUrlTree(['/bisq/block/', hash]).toString()
|
||||
);
|
||||
this.seoService.updateCanonical(this.location.path());
|
||||
return this.bisqApiService.getBlock$(this.blockHash)
|
||||
.pipe(catchError(this.caughtHttpError.bind(this)));
|
||||
}),
|
||||
|
||||
@@ -30,7 +30,7 @@ export class BisqDashboardComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`:@@meta.title.bisq.markets:Markets`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open Project™. See Bisq market prices, trading activity, and more.`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See Bisq market prices, trading activity, and more.`);
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.volumes$ = this.bisqApiService.getAllVolumesDay$()
|
||||
|
||||
@@ -27,9 +27,11 @@ import { AutofocusDirective } from '../components/ngx-bootstrap-multiselect/auto
|
||||
import { MultiSelectSearchFilter } from '../components/ngx-bootstrap-multiselect/search-filter.pipe';
|
||||
import { OffClickDirective } from '../components/ngx-bootstrap-multiselect/off-click.directive';
|
||||
import { NgxDropdownMultiselectComponent } from '../components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component';
|
||||
import { BisqMasterPageComponent } from '../components/bisq-master-page/bisq-master-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
BisqMasterPageComponent,
|
||||
BisqTransactionsComponent,
|
||||
BisqTransactionComponent,
|
||||
BisqBlockComponent,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AboutComponent } from '../components/about/about.component';
|
||||
import { BisqMasterPageComponent } from '../components/bisq-master-page/bisq-master-page.component';
|
||||
import { BisqTransactionsComponent } from './bisq-transactions/bisq-transactions.component';
|
||||
import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.component';
|
||||
import { BisqBlockComponent } from './bisq-block/bisq-block.component';
|
||||
@@ -10,78 +10,83 @@ import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
|
||||
import { BisqDashboardComponent } from './bisq-dashboard/bisq-dashboard.component';
|
||||
import { BisqMarketComponent } from './bisq-market/bisq-market.component';
|
||||
import { BisqMainDashboardComponent } from './bisq-main-dashboard/bisq-main-dashboard.component';
|
||||
import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component';
|
||||
import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: BisqMainDashboardComponent,
|
||||
},
|
||||
{
|
||||
path: 'markets',
|
||||
data: { networks: ['bisq'] },
|
||||
component: BisqDashboardComponent,
|
||||
},
|
||||
{
|
||||
path: 'transactions',
|
||||
data: { networks: ['bisq'] },
|
||||
component: BisqTransactionsComponent
|
||||
},
|
||||
{
|
||||
path: 'market/:pair',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqMarketComponent,
|
||||
},
|
||||
{
|
||||
path: 'tx/push',
|
||||
component: PushTransactionComponent,
|
||||
},
|
||||
{
|
||||
path: 'tx/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqTransactionComponent
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
children: [],
|
||||
component: BisqBlocksComponent
|
||||
},
|
||||
{
|
||||
path: 'block/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqBlockComponent,
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqAddressComponent,
|
||||
},
|
||||
{
|
||||
path: 'stats',
|
||||
data: { networks: ['bisq'] },
|
||||
component: BisqStatsComponent,
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
component: AboutComponent,
|
||||
},
|
||||
{
|
||||
path: 'docs',
|
||||
loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
{
|
||||
path: 'terms-of-service',
|
||||
component: TermsOfServiceComponent
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
}
|
||||
{
|
||||
path: '',
|
||||
component: BisqMasterPageComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: BisqMainDashboardComponent,
|
||||
},
|
||||
{
|
||||
path: 'markets',
|
||||
data: { networks: ['bisq'] },
|
||||
component: BisqDashboardComponent,
|
||||
},
|
||||
{
|
||||
path: 'transactions',
|
||||
data: { networks: ['bisq'] },
|
||||
component: BisqTransactionsComponent
|
||||
},
|
||||
{
|
||||
path: 'market/:pair',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqMarketComponent,
|
||||
},
|
||||
{
|
||||
path: 'tx/push',
|
||||
component: PushTransactionComponent,
|
||||
},
|
||||
{
|
||||
path: 'tx/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqTransactionComponent
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
children: [],
|
||||
component: BisqBlocksComponent
|
||||
},
|
||||
{
|
||||
path: 'block/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqBlockComponent,
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqAddressComponent,
|
||||
},
|
||||
{
|
||||
path: 'stats',
|
||||
data: { networks: ['bisq'] },
|
||||
component: BisqStatsComponent,
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
loadChildren: () => import('../components/about/about.module').then(m => m.AboutModule),
|
||||
},
|
||||
{
|
||||
path: 'docs',
|
||||
loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
{
|
||||
path: 'terms-of-service',
|
||||
loadChildren: () => import('../components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule),
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
37
frontend/src/app/bitcoin-graphs.module.ts
Normal file
37
frontend/src/app/bitcoin-graphs.module.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { MasterPageComponent } from './components/master-page/master-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: MasterPageComponent,
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule),
|
||||
data: { preload: true },
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes)
|
||||
],
|
||||
exports: [
|
||||
RouterModule
|
||||
]
|
||||
})
|
||||
export class BitcoinGraphsRoutingModule { }
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
BitcoinGraphsRoutingModule,
|
||||
],
|
||||
})
|
||||
export class BitcoinGraphsModule { }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -225,7 +225,7 @@ const witnessSize = (vin: Vin) => vin.witness ? vin.witness.reduce((S, w) => S +
|
||||
const scriptSigSize = (vin: Vin) => vin.scriptsig ? vin.scriptsig.length / 2 : 0;
|
||||
|
||||
// Power of ten wrapper
|
||||
export function selectPowerOfTen(val: number): { divider: number, unit: string } {
|
||||
export function selectPowerOfTen(val: number, multiplier = 1): { divider: number, unit: string } {
|
||||
const powerOfTen = {
|
||||
exa: Math.pow(10, 18),
|
||||
peta: Math.pow(10, 15),
|
||||
@@ -236,17 +236,17 @@ export function selectPowerOfTen(val: number): { divider: number, unit: string }
|
||||
};
|
||||
|
||||
let selectedPowerOfTen: { divider: number, unit: string };
|
||||
if (val < powerOfTen.kilo) {
|
||||
if (val < powerOfTen.kilo * multiplier) {
|
||||
selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling
|
||||
} else if (val < powerOfTen.mega) {
|
||||
} else if (val < powerOfTen.mega * multiplier) {
|
||||
selectedPowerOfTen = { divider: powerOfTen.kilo, unit: 'k' };
|
||||
} else if (val < powerOfTen.giga) {
|
||||
} else if (val < powerOfTen.giga * multiplier) {
|
||||
selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' };
|
||||
} else if (val < powerOfTen.tera) {
|
||||
} else if (val < powerOfTen.tera * multiplier) {
|
||||
selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' };
|
||||
} else if (val < powerOfTen.peta) {
|
||||
} else if (val < powerOfTen.peta * multiplier) {
|
||||
selectedPowerOfTen = { divider: powerOfTen.tera, unit: 'T' };
|
||||
} else if (val < powerOfTen.exa) {
|
||||
} else if (val < powerOfTen.exa * multiplier) {
|
||||
selectedPowerOfTen = { divider: powerOfTen.peta, unit: 'P' };
|
||||
} else {
|
||||
selectedPowerOfTen = { divider: powerOfTen.exa, unit: 'E' };
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<div id="become-sponsor-container" [ngClass]="context">
|
||||
<div class="become-sponsor community">
|
||||
<p style="font-weight: 700; font-size: 18px;">If you're an individual...</p>
|
||||
<a [href]="host + '/sponsor'" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button" (click)="onSponsorClick($event)">Become a Community Sponsor</a>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Exclusive swag</p>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Your avatar on the About page</p>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> And more coming soon :)</p>
|
||||
</div>
|
||||
<div class="become-sponsor enterprise">
|
||||
<p style="font-weight: 700; font-size: 18px;">If you're a business...</p>
|
||||
<a [href]="host + '/enterprise'" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button" (click)="onEnterpriseClick($event)">Become an Enterprise Sponsor</a>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Increased API limits</p>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Co-branded instance</p>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> 99% service-level agreement</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,50 @@
|
||||
#become-sponsor-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin: 68px auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#become-sponsor-container.account {
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.become-sponsor {
|
||||
background-color: #1d1f31;
|
||||
border-radius: 16px;
|
||||
padding: 12px 20px;
|
||||
width: 400px;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.become-sponsor a {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#become-sponsor-container .btn {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
#become-sponsor-container .ng-fa-icon {
|
||||
color: #2ecc71;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
#become-sponsor-container .sponsor-feature {
|
||||
text-align: left;
|
||||
width: 250px;
|
||||
margin: 12px auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
|
||||
#become-sponsor-container {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about-sponsors',
|
||||
templateUrl: './about-sponsors.component.html',
|
||||
styleUrls: ['./about-sponsors.component.scss'],
|
||||
})
|
||||
export class AboutSponsorsComponent {
|
||||
@Input() host = 'https://mempool.space';
|
||||
@Input() context = 'about';
|
||||
|
||||
constructor(private enterpriseService: EnterpriseService) {
|
||||
}
|
||||
|
||||
onSponsorClick(e): boolean {
|
||||
this.enterpriseService.goal(5);
|
||||
return true;
|
||||
}
|
||||
|
||||
onEnterpriseClick(e): boolean {
|
||||
this.enterpriseService.goal(6);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
|
||||
<div class="about-text">
|
||||
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template></h5>
|
||||
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ®</ng-template></h5>
|
||||
<p i18n>Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.</p>
|
||||
</div>
|
||||
|
||||
@@ -32,12 +32,8 @@
|
||||
<track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null">
|
||||
</video>
|
||||
|
||||
<ng-container *ngIf="false && officialMempoolSpace">
|
||||
<h3 class="mt-5">Sponsor the project</h3>
|
||||
<div class="d-flex justify-content-center" style="max-width: 90%; margin: 35px auto 75px auto; column-gap: 15px">
|
||||
<a href="/sponsor" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button">Community</a>
|
||||
<a href="/enterprise" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button">Enterprise</a>
|
||||
</div>
|
||||
<ng-container *ngIf="officialMempoolSpace">
|
||||
<app-about-sponsors></app-about-sponsors>
|
||||
</ng-container>
|
||||
|
||||
<div class="enterprise-sponsor" id="enterprise-sponsors">
|
||||
@@ -186,14 +182,14 @@
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="officialMempoolSpace">
|
||||
<div *ngIf="profiles$ | async as profiles" id="community-sponsors">
|
||||
<div *ngIf="profiles$ | async as profiles" id="community-sponsors-anchor">
|
||||
<div class="community-sponsor" style="margin-bottom: 68px" *ngIf="profiles.whales.length > 0">
|
||||
<h3 i18n="about.sponsors.withHeart">Whale Sponsors</h3>
|
||||
<div class="wrapper">
|
||||
<ng-container>
|
||||
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
|
||||
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
|
||||
<img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||
</a>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
@@ -205,7 +201,7 @@
|
||||
<div class="wrapper">
|
||||
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
|
||||
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
|
||||
<img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||
</a>
|
||||
</ng-template>
|
||||
</div>
|
||||
@@ -299,9 +295,9 @@
|
||||
<img class="image" src="/resources/profile/blixt.png" />
|
||||
<span>Blixt</span>
|
||||
</a>
|
||||
<a href="https://github.com/ZeusLN/zeus" target="_blank" title="Zeus">
|
||||
<a href="https://github.com/ZeusLN/zeus" target="_blank" title="ZEUS">
|
||||
<img class="image" src="/resources/profile/zeus.png" />
|
||||
<span>Zeus</span>
|
||||
<span>ZEUS</span>
|
||||
</a>
|
||||
<a href="https://github.com/vulpemventures/marina" target="_blank" title="Marina Wallet">
|
||||
<img class="image" src="/resources/profile/marina.svg" />
|
||||
@@ -409,38 +405,30 @@
|
||||
|
||||
<div class="copyright">
|
||||
<div class="title">
|
||||
Copyright © 2019-2023<br>
|
||||
The Mempool Open Source Project
|
||||
Copyright © 2019-2024<br>
|
||||
Mempool Space K.K.<br>
|
||||
and other shadowy super-coders
|
||||
</div>
|
||||
<p>
|
||||
<a href="https://github.com/mempool/mempool">The Mempool Open Source Project</a> is free software; you can redistribute it and/or modify it under the terms of (at your option) either:<br>
|
||||
<a href="https://github.com/mempool/mempool">The Mempool Open Source Project</a> is free software; you can redistribute it and/or modify it under the terms of the <a href="https://www.gnu.org/licenses/agpl-3.0-standalone.html">GNU Affero General Public License</a> as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>.<br>
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
1) the <a href="https://www.gnu.org/licenses/agpl-3.0-standalone.html">GNU Affero General Public License</a> as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>; or<br>
|
||||
</li>
|
||||
<li>
|
||||
2) the <a href="https://www.gnu.org/licenses/gpl-3.0-standalone.html">GNU General Public License</a> as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>.<br>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the full license terms for more details.<br>
|
||||
</p>
|
||||
<p>
|
||||
This program incorporates software and other components licensed from third parties. See the full list of <a href="/3rdpartylicenses.txt">Third-Party Licenses</a> for legal notices from those projects.
|
||||
</p>
|
||||
<div class="title">
|
||||
Trademark Notice<br>
|
||||
</div>
|
||||
<p>
|
||||
The Mempool Open Source Project®, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks logo™ are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool logo, the mempool Square logo, the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
</p>
|
||||
<p>
|
||||
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on <https://mempool.space/trademark-policy>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer-links">
|
||||
<a href="/3rdpartylicenses.txt">Third-party Licenses</a>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { map, share, tap } from 'rxjs/operators';
|
||||
import { ITranslators } from '../../interfaces/node-api.interface';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about',
|
||||
@@ -33,6 +34,7 @@ export class AboutComponent implements OnInit {
|
||||
private websocketService: WebsocketService,
|
||||
private seoService: SeoService,
|
||||
public stateService: StateService,
|
||||
private enterpriseService: EnterpriseService,
|
||||
private apiService: ApiService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
@@ -43,12 +45,17 @@ export class AboutComponent implements OnInit {
|
||||
ngOnInit() {
|
||||
this.backendInfo$ = this.stateService.backendInfo$;
|
||||
this.seoService.setTitle($localize`:@@004b222ff9ef9dd4771b777950ca1d0e4cd4348a:About`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.about:Learn more about The Mempool Open Source Project™\: enterprise sponsors, individual sponsors, integrations, who contributes, FOSS licensing, and more.`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.about:Learn more about The Mempool Open Source Project®\: enterprise sponsors, individual sponsors, integrations, who contributes, FOSS licensing, and more.`);
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.profiles$ = this.apiService.getAboutPageProfiles$().pipe(
|
||||
tap(() => {
|
||||
this.goToAnchor()
|
||||
tap((profiles: any) => {
|
||||
const scrollToSponsors = this.route.snapshot.fragment === 'community-sponsors';
|
||||
if (scrollToSponsors && !profiles?.whales?.length && !profiles?.chads?.length) {
|
||||
return;
|
||||
} else {
|
||||
this.goToAnchor(scrollToSponsors)
|
||||
}
|
||||
}),
|
||||
share(),
|
||||
)
|
||||
@@ -83,11 +90,19 @@ export class AboutComponent implements OnInit {
|
||||
this.goToAnchor();
|
||||
}
|
||||
|
||||
goToAnchor() {
|
||||
goToAnchor(scrollToSponsor = false) {
|
||||
if (!scrollToSponsor) {
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (this.route.snapshot.fragment) {
|
||||
if (this.document.getElementById(this.route.snapshot.fragment)) {
|
||||
this.document.getElementById(this.route.snapshot.fragment).scrollIntoView({behavior: 'smooth'});
|
||||
const el = scrollToSponsor ? this.document.getElementById('community-sponsors-anchor') : this.document.getElementById(this.route.snapshot.fragment);
|
||||
if (el) {
|
||||
if (scrollToSponsor) {
|
||||
el.scrollIntoView({behavior: 'smooth', block: 'center', inline: 'center'});
|
||||
} else {
|
||||
el.scrollIntoView({behavior: 'smooth'});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 1);
|
||||
@@ -108,4 +123,14 @@ export class AboutComponent implements OnInit {
|
||||
unmutePromoVideo(): void {
|
||||
this.promoVideo.nativeElement.muted = false;
|
||||
}
|
||||
|
||||
onSponsorClick(e): boolean {
|
||||
this.enterpriseService.goal(5);
|
||||
return true;
|
||||
}
|
||||
|
||||
onEnterpriseClick(e): boolean {
|
||||
this.enterpriseService.goal(6);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
45
frontend/src/app/components/about/about.module.ts
Normal file
45
frontend/src/app/components/about/about.module.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { AboutComponent } from './about.component';
|
||||
import { AboutSponsorsComponent } from './about-sponsors.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AboutComponent,
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes)
|
||||
],
|
||||
exports: [
|
||||
RouterModule
|
||||
]
|
||||
})
|
||||
export class AboutRoutingModule { }
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
AboutRoutingModule,
|
||||
SharedModule,
|
||||
],
|
||||
declarations: [
|
||||
AboutComponent,
|
||||
AboutSponsorsComponent,
|
||||
],
|
||||
exports: [
|
||||
AboutSponsorsComponent,
|
||||
]
|
||||
})
|
||||
export class AboutModule { }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
height: 100%;
|
||||
min-width: 120px;
|
||||
width: 120px;
|
||||
max-height: 90vh;
|
||||
margin-left: 4em;
|
||||
margin-right: 1.5em;
|
||||
padding-bottom: 63px;
|
||||
@@ -18,6 +17,7 @@
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-height: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
@@ -58,13 +58,15 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
|
||||
fee: option.fee,
|
||||
}
|
||||
});
|
||||
bars.push({
|
||||
rate: this.estimate.targetFeeRate,
|
||||
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
|
||||
class: 'target',
|
||||
label: 'next block',
|
||||
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
|
||||
});
|
||||
if (this.estimate.nextBlockFee > this.estimate.txSummary.effectiveFee) {
|
||||
bars.push({
|
||||
rate: this.estimate.targetFeeRate,
|
||||
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
|
||||
class: 'target',
|
||||
label: 'next block',
|
||||
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
|
||||
});
|
||||
}
|
||||
bars.push({
|
||||
rate: baseRate,
|
||||
style: this.getStyle(baseRate, maxRate, 0),
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<span id="successAlert" class="m-0 p-0 d-block" style="height: 1px;"></span>
|
||||
<div class="row" *ngIf="showSuccess">
|
||||
<div class="col" id="successAlert">
|
||||
<div class="col">
|
||||
<div class="alert alert-success">
|
||||
Transaction has now been submitted to mining pools for acceleration. You can track the progress <a class="alert-link" routerLink="/services/accelerator/history">here</a>.
|
||||
Transaction has now been <a class="alert-link" routerLink="/services/accelerator/history">submitted</a> to mining pools for acceleration.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span id="mempoolError" class="m-0 p-0 d-block" style="height: 1px;"></span>
|
||||
<div class="row" *ngIf="error">
|
||||
<div class="col" id="mempoolError">
|
||||
<app-mempool-error [error]="error"></app-mempool-error>
|
||||
<div class="col">
|
||||
<app-mempool-error [error]="error" [alertClass]="error === 'waitlisted' ? 'alert-mempool' : 'alert-danger'"></app-mempool-error>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,8 +25,13 @@
|
||||
></app-accelerate-fee-graph>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="estimate">
|
||||
<ng-container *ngIf="estimate else loadingEstimate">
|
||||
<div [class]="{estimateDisabled: error}">
|
||||
|
||||
<div *ngIf="user && !estimate.hasAccess">
|
||||
<div class="alert alert-mempool">You are currently on the waitlist</div>
|
||||
</div>
|
||||
|
||||
<h5>Your transaction</h5>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
@@ -37,10 +44,10 @@
|
||||
<td class="item">
|
||||
Virtual size
|
||||
</td>
|
||||
<td class="units" [innerHTML]="'‎' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td>
|
||||
<td style="text-align: end;" [innerHTML]="'‎' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td>
|
||||
</tr>
|
||||
<tr class="info">
|
||||
<td class="info">
|
||||
<td class="info" colspan=3>
|
||||
<i><small>Size in vbytes of this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -48,12 +55,12 @@
|
||||
<td class="item">
|
||||
In-band fees
|
||||
</td>
|
||||
<td class="units">
|
||||
{{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<td style="text-align: end;">
|
||||
{{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats">sats</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="info group-last">
|
||||
<td class="info">
|
||||
<td class="info" colspan=3>
|
||||
<i><small>Fees already paid by this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -74,8 +81,8 @@
|
||||
<div class="d-flex mb-0">
|
||||
<ng-container *ngFor="let option of maxRateOptions">
|
||||
<button type="button" class="btn btn-primary flex-grow-1 btn-border btn-sm feerate" [class]="{active: selectFeeRateIndex === option.index}" (click)="setUserBid(option)">
|
||||
<span class="fee">{{ option.fee | number }} <span class="symbol" i18n="shared.sats|sats">sats</span></span>
|
||||
<span class="rate">~ <app-fee-rate [fee]="option.rate" rounding="1.0-0"></app-fee-rate></span>
|
||||
<span class="fee">{{ option.fee + estimate.mempoolBaseFee + estimate.vsizeFee | number }} <span class="symbol" i18n="shared.sats">sats</span></span>
|
||||
<span class="rate">~<app-fee-rate [fee]="option.rate" rounding="1.0-0"></app-fee-rate></span>
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -87,23 +94,15 @@
|
||||
<h5>Acceleration summary</h5>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<div class="table-toggle btn-group btn-group-toggle">
|
||||
<div class="btn btn-primary btn-sm" [class.active]="showTable === 'estimated'" (click)="showTable = 'estimated'">
|
||||
<span>Estimated cost</span>
|
||||
</div>
|
||||
<div class="btn btn-primary btn-sm" [class.active]="showTable === 'maximum'" (click)="showTable = 'maximum'">
|
||||
<span>Maximum cost</span>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-borderless table-border table-dark table-accelerator">
|
||||
<tbody>
|
||||
<!-- ESTIMATED FEE -->
|
||||
<ng-container *ngIf="showTable === 'estimated'">
|
||||
<ng-container>
|
||||
<tr class="group-first">
|
||||
<td class="item">
|
||||
Next block market rate
|
||||
</td>
|
||||
<td class="amt" style="font-size: 20px">
|
||||
<td class="amt" style="font-size: 16px">
|
||||
{{ estimate.targetFeeRate | number : '1.0-0' }}
|
||||
</td>
|
||||
<td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
|
||||
@@ -116,34 +115,8 @@
|
||||
{{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }}
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<span class="fiat"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<!-- USER MAX BID -->
|
||||
<ng-container *ngIf="showTable === 'maximum'">
|
||||
<tr class="group-first">
|
||||
<td class="item">
|
||||
Your maximum
|
||||
</td>
|
||||
<td class="amt" style="width: 45%; font-size: 20px">
|
||||
~{{ ((estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }}
|
||||
</td>
|
||||
<td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
|
||||
</tr>
|
||||
<tr class="info">
|
||||
<td class="info">
|
||||
<i><small>The maximum extra transaction fee you could pay</small></i>
|
||||
</td>
|
||||
<td class="amt">
|
||||
<span>
|
||||
{{ userBid | number }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<span class="fiat"><app-fiat [value]="userBid"></app-fiat></span>
|
||||
<span class="symbol" i18n="shared.sats">sats</span>
|
||||
<span class="fiat ml-1"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
@@ -156,32 +129,32 @@
|
||||
</tr>
|
||||
<tr class="info">
|
||||
<td class="info">
|
||||
<i><small>mempool.space fee</small></i>
|
||||
<i><small>Accelerator Service Fee</small></i>
|
||||
</td>
|
||||
<td class="amt">
|
||||
+{{ estimate.mempoolBaseFee | number }}
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<span class="fiat"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span>
|
||||
<span class="symbol" i18n="shared.sats">sats</span>
|
||||
<span class="fiat ml-1"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
|
||||
<tr class="info group-last">
|
||||
<td class="info">
|
||||
<i><small>Transaction vsize fee</small></i>
|
||||
<i><small>Transaction Size Surcharge</small></i>
|
||||
</td>
|
||||
<td class="amt">
|
||||
+{{ estimate.vsizeFee | number }}
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<span class="fiat"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span>
|
||||
<span class="symbol" i18n="shared.sats">sats</span>
|
||||
<span class="fiat ml-1"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- NEXT BLOCK ESTIMATE -->
|
||||
<ng-container *ngIf="showTable === 'estimated'">
|
||||
<tr class="group-first">
|
||||
<ng-container>
|
||||
<tr class="group-first" style="border-top: 1px dashed grey; border-collapse: collapse;">
|
||||
<td class="item">
|
||||
<b style="background-color: #5E35B1" class="p-1 pl-0">Estimated acceleration cost</b>
|
||||
</td>
|
||||
@@ -191,19 +164,19 @@
|
||||
</span>
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<span class="fiat"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span>
|
||||
<span class="symbol" i18n="shared.sats">sats</span>
|
||||
<span class="fiat ml-1"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="info group-last">
|
||||
<td class="info">
|
||||
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
|
||||
<td class="info" colspan=3>
|
||||
<i><small>If your tx is accelerated to </small><small>{{ estimate.targetFeeRate | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
|
||||
<!-- MAX COST -->
|
||||
<ng-container *ngIf="showTable === 'maximum'">
|
||||
<ng-container>
|
||||
<tr class="group-first">
|
||||
<td class="item">
|
||||
<b style="background-color: #105fb0;" class="p-1 pl-0">Maximum acceleration cost</b>
|
||||
@@ -214,21 +187,21 @@
|
||||
</span>
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<span class="fiat">
|
||||
<span class="symbol" i18n="shared.sats">sats</span>
|
||||
<span class="fiat ml-1">
|
||||
<app-fiat [value]="maxCost" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="info group-last">
|
||||
<td class="info">
|
||||
<td class="info" colspan=3>
|
||||
<i><small>If your tx is accelerated to </small><small>~{{ ((estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
|
||||
<!-- USER BALANCE -->
|
||||
<ng-container *ngIf="estimate.userBalance < maxCost">
|
||||
<ng-container *ngIf="isLoggedIn() && estimate.userBalance < maxCost">
|
||||
<tr class="group-first group-last" style="border-top: 1px dashed grey">
|
||||
<td class="item">
|
||||
Available balance
|
||||
@@ -237,13 +210,24 @@
|
||||
{{ estimate.userBalance | number }}
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<span class="fiat">
|
||||
<span class="symbol" i18n="shared.sats">sats</span>
|
||||
<span class="fiat ml-1">
|
||||
<app-fiat [value]="estimate.userBalance" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
|
||||
<!-- LOGIN CTA -->
|
||||
<ng-container *ngIf="!isLoggedIn()">
|
||||
<tr class="group-first group-last" style="border-top: 1px dashed grey">
|
||||
<td class="item"></td>
|
||||
<td class="amt"></td>
|
||||
<td class="units d-flex">
|
||||
<a [routerLink]="['/login']" [queryParams]="{redirectTo: '/tx/' + tx.txid + '#accelerate'}" class="btn btn-purple flex-grow-1">Login</a>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -251,7 +235,7 @@
|
||||
|
||||
<div class="row mb-3" *ngIf="isLoggedIn()">
|
||||
<div class="col">
|
||||
<div class="d-flex justify-content-end">
|
||||
<div class="d-flex justify-content-end" *ngIf="user && estimate.hasAccess">
|
||||
<button class="btn btn-sm btn-primary btn-success" style="width: 150px" (click)="accelerate()">Accelerate</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -259,4 +243,9 @@
|
||||
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingEstimate>
|
||||
<div class="skeleton-loader"></div>
|
||||
<br>
|
||||
</ng-template>
|
||||
@@ -8,9 +8,6 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.fee {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.rate {
|
||||
font-size: 0.9em;
|
||||
.symbol {
|
||||
@@ -28,7 +25,10 @@
|
||||
.feerate.active {
|
||||
background-color: #105fb0 !important;
|
||||
opacity: 1;
|
||||
border: 1px solid white !important;
|
||||
border: 1px solid #007fff !important;
|
||||
}
|
||||
.feerate:focus {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.estimateDisabled {
|
||||
@@ -41,10 +41,26 @@
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.tab {
|
||||
&:first-child {
|
||||
margin-right: 1px;
|
||||
}
|
||||
border: solid 1px black;
|
||||
border-bottom: none;
|
||||
background-color: #323655;
|
||||
border-top-left-radius: 10px !important;
|
||||
border-top-right-radius: 10px !important;
|
||||
}
|
||||
.tab.active {
|
||||
background-color: #5d659d !important;
|
||||
opacity: 1;
|
||||
}
|
||||
.tab:focus {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.table-accelerator {
|
||||
tr {
|
||||
text-wrap: wrap;
|
||||
|
||||
td {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
@@ -68,6 +84,7 @@
|
||||
}
|
||||
&.info {
|
||||
color: #6c757d;
|
||||
white-space: initial;
|
||||
}
|
||||
&.amt {
|
||||
text-align: right;
|
||||
@@ -76,6 +93,9 @@
|
||||
&.units {
|
||||
padding-left: 0.2em;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,4 +105,8 @@
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.item {
|
||||
white-space: initial;
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener } from '@angular/core';
|
||||
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener, ChangeDetectorRef } from '@angular/core';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { Subscription, catchError, of, tap } from 'rxjs';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { nextRoundNumber } from '../../shared/common.utils';
|
||||
import { ServicesApiServices } from '../../services/services-api.service';
|
||||
import { AudioService } from '../../services/audio.service';
|
||||
|
||||
export type AccelerationEstimate = {
|
||||
txSummary: TxSummary;
|
||||
@@ -55,14 +57,16 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
maxCost = 0;
|
||||
userBid = 0;
|
||||
selectFeeRateIndex = 1;
|
||||
showTable: 'estimated' | 'maximum' = 'maximum';
|
||||
isMobile: boolean = window.innerWidth <= 767.98;
|
||||
user: any = undefined;
|
||||
|
||||
maxRateOptions: RateOption[] = [];
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private storageService: StorageService
|
||||
private servicesApiService: ServicesApiServices,
|
||||
private storageService: StorageService,
|
||||
private audioService: AudioService,
|
||||
private cd: ChangeDetectorRef
|
||||
) { }
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -73,12 +77,14 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.scrollEvent) {
|
||||
this.scrollToPreview('acceleratePreviewAnchor', 'center');
|
||||
this.scrollToPreview('acceleratePreviewAnchor', 'start');
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.estimateSubscription = this.apiService.estimate$(this.tx.txid).pipe(
|
||||
this.user = this.storageService.getAuth()?.user ?? null;
|
||||
|
||||
this.estimateSubscription = this.servicesApiService.estimate$(this.tx.txid).pipe(
|
||||
tap((response) => {
|
||||
if (response.status === 204) {
|
||||
this.estimate = undefined;
|
||||
@@ -93,7 +99,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
this.estimateSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
if (this.estimate.userBalance <= 0) {
|
||||
if (this.estimate.hasAccess === true && this.estimate.userBalance <= 0) {
|
||||
if (this.isLoggedIn()) {
|
||||
this.error = `not_enough_balance`;
|
||||
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
||||
@@ -126,7 +132,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
|
||||
|
||||
if (!this.error) {
|
||||
this.scrollToPreview('acceleratePreviewAnchor', 'center');
|
||||
this.scrollToPreview('acceleratePreviewAnchor', 'start');
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -162,13 +168,14 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
scrollToPreview(id: string, position: ScrollLogicalPosition) {
|
||||
const acceleratePreviewAnchor = document.getElementById(id);
|
||||
if (acceleratePreviewAnchor) {
|
||||
this.cd.markForCheck();
|
||||
acceleratePreviewAnchor.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
inline: position,
|
||||
block: position,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send acceleration request
|
||||
@@ -177,17 +184,22 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
if (this.accelerationSubscription) {
|
||||
this.accelerationSubscription.unsubscribe();
|
||||
}
|
||||
this.accelerationSubscription = this.apiService.accelerate$(
|
||||
this.accelerationSubscription = this.servicesApiService.accelerate$(
|
||||
this.tx.txid,
|
||||
this.userBid
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
this.showSuccess = true;
|
||||
this.scrollToPreviewWithTimeout('successAlert', 'center');
|
||||
this.estimateSubscription.unsubscribe();
|
||||
},
|
||||
error: (response) => {
|
||||
this.error = response.error;
|
||||
if (response.status === 403 && response.error === 'not_available') {
|
||||
this.error = 'waitlisted';
|
||||
} else {
|
||||
this.error = response.error;
|
||||
}
|
||||
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
||||
}
|
||||
});
|
||||
@@ -202,4 +214,4 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
onResize(): void {
|
||||
this.isMobile = window.innerWidth <= 767.98;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
|
||||
|
||||
<div [class.full-container]="!widget">
|
||||
<div *ngIf="!widget" class="card-header mb-0 mb-md-4">
|
||||
<div class="d-flex d-md-block align-items-baseline">
|
||||
<span i18n="accelerator.acceleration-fees">Acceleration Fees</span>
|
||||
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
|
||||
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
||||
<div class="btn-group btn-group-toggle" name="radioBasic" [class]="{'disabled': isLoading}">
|
||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
|
||||
<input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" formControlName="dateSpan"> 24H
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3d'">
|
||||
<input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" formControlName="dateSpan"> 3D
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
|
||||
<input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" formControlName="dateSpan"> 1W
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
|
||||
<input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" formControlName="dateSpan"> 1M
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
|
||||
<input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" formControlName="dateSpan"> 3M
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div [class.chart]="!widget" [class.chart-widget]="widget" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,71 @@
|
||||
.card-header {
|
||||
border-bottom: 0;
|
||||
font-size: 18px;
|
||||
@media (min-width: 465px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.full-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-bottom: 20px;
|
||||
padding-right: 10px;
|
||||
@media (max-width: 992px) {
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
@media (max-width: 829px) {
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
@media (max-width: 629px) {
|
||||
padding-bottom: 55px;
|
||||
}
|
||||
@media (max-width: 567px) {
|
||||
padding-bottom: 55px;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1rem;
|
||||
color: #4a68b9;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnDestroy, OnInit } from '@angular/core';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { Observable, Subscription, combineLatest, fromEvent } from 'rxjs';
|
||||
import { startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../../shared/graphs.utils';
|
||||
import { StorageService } from '../../../services/storage.service';
|
||||
import { MiningService } from '../../../services/mining.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Acceleration } from '../../../interfaces/node-api.interface';
|
||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-acceleration-fees-graph',
|
||||
templateUrl: './acceleration-fees-graph.component.html',
|
||||
styleUrls: ['./acceleration-fees-graph.component.scss'],
|
||||
styles: [`
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
|
||||
@Input() widget: boolean = false;
|
||||
@Input() height: number = 300;
|
||||
@Input() right: number | string = 45;
|
||||
@Input() left: number | string = 75;
|
||||
@Input() accelerations$: Observable<Acceleration[]>;
|
||||
|
||||
miningWindowPreference: string;
|
||||
radioGroupForm: UntypedFormGroup;
|
||||
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
};
|
||||
|
||||
hrStatsObservable$: Observable<any>;
|
||||
statsObservable$: Observable<any>;
|
||||
statsSubscription: Subscription;
|
||||
isLoading = true;
|
||||
formatNumber = formatNumber;
|
||||
timespan = '';
|
||||
chartInstance: any = undefined;
|
||||
|
||||
currency: string;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private seoService: SeoService,
|
||||
private servicesApiService: ServicesApiServices,
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private storageService: StorageService,
|
||||
private miningService: MiningService,
|
||||
private route: ActivatedRoute,
|
||||
private cd: ChangeDetectorRef,
|
||||
) {
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
|
||||
this.radioGroupForm.controls.dateSpan.setValue('1y');
|
||||
this.currency = 'USD';
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.widget) {
|
||||
this.miningWindowPreference = '3m';
|
||||
} else {
|
||||
this.seoService.setTitle($localize`:@@bcf34abc2d9ed8f45a2f65dd464c46694e9a181e:Acceleration Fees`);
|
||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('3m');
|
||||
}
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
||||
|
||||
this.route.fragment.subscribe((fragment) => {
|
||||
if (['24h', '3d', '1w', '1m', '3m'].indexOf(fragment) > -1) {
|
||||
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
|
||||
}
|
||||
});
|
||||
this.statsObservable$ = combineLatest([
|
||||
this.radioGroupForm.get('dateSpan').valueChanges.pipe(
|
||||
startWith(this.radioGroupForm.controls.dateSpan.value),
|
||||
switchMap((timespan) => {
|
||||
if (!this.widget) {
|
||||
this.storageService.setValue('miningWindowPreference', timespan);
|
||||
}
|
||||
this.isLoading = true;
|
||||
this.timespan = timespan;
|
||||
return this.servicesApiService.getAggregatedAccelerationHistory$({timeframe: this.timespan});
|
||||
})
|
||||
),
|
||||
fromEvent(window, 'resize').pipe(startWith(null)),
|
||||
]).pipe(
|
||||
tap(([history]) => {
|
||||
this.isLoading = false;
|
||||
this.prepareChartOptions(history);
|
||||
this.cd.markForCheck();
|
||||
})
|
||||
);
|
||||
|
||||
this.statsObservable$.subscribe();
|
||||
}
|
||||
|
||||
prepareChartOptions(data) {
|
||||
let title: object;
|
||||
if (data.length === 0) {
|
||||
title = {
|
||||
textStyle: {
|
||||
color: 'grey',
|
||||
fontSize: 15
|
||||
},
|
||||
text: $localize`No accelerated transaction for this timeframe`,
|
||||
left: 'center',
|
||||
top: 'center'
|
||||
};
|
||||
}
|
||||
|
||||
this.chartOptions = {
|
||||
title: title,
|
||||
color: [
|
||||
'#8F5FF6',
|
||||
'#6b6b6b',
|
||||
],
|
||||
animation: false,
|
||||
grid: {
|
||||
height: (this.widget && this.height) ? this.height - 30 : undefined,
|
||||
top: this.widget ? 20 : 40,
|
||||
bottom: this.widget ? 30 : 80,
|
||||
right: this.right,
|
||||
left: this.left,
|
||||
},
|
||||
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) => {
|
||||
let tooltip = `<b style="color: white; margin-left: 2px">${formatterXAxis(this.locale, this.timespan, parseInt(ticks[0].axisValue, 10))}</b><br>`;
|
||||
|
||||
if (ticks[0].data[1] > 10_000_000) {
|
||||
tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].data[1] / 100_000_000, this.locale, '1.0-0')} BTC<br>`;
|
||||
} else {
|
||||
tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].data[1], this.locale, '1.0-0')} sats<br>`;
|
||||
}
|
||||
|
||||
if (['24h', '3d'].includes(this.timespan)) {
|
||||
tooltip += `<small>` + $localize`At block: ${ticks[0].data[2]}` + `</small>`;
|
||||
} else {
|
||||
tooltip += `<small>` + $localize`Around block: ${ticks[0].data[2]}` + `</small>`;
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
},
|
||||
xAxis: data.length === 0 ? undefined :
|
||||
{
|
||||
name: this.widget ? undefined : formatterXAxisLabel(this.locale, this.timespan),
|
||||
nameLocation: 'middle',
|
||||
nameTextStyle: {
|
||||
padding: [10, 0, 0, 0],
|
||||
},
|
||||
type: 'time',
|
||||
boundaryGap: false,
|
||||
axisLine: { onZero: true },
|
||||
axisLabel: {
|
||||
formatter: val => formatterXAxisTimeCategory(this.locale, this.timespan, parseInt(val, 10)),
|
||||
align: 'center',
|
||||
fontSize: 11,
|
||||
lineHeight: 12,
|
||||
hideOverlap: true,
|
||||
padding: [0, 5],
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: [
|
||||
{
|
||||
name: 'Total bid boost',
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
},
|
||||
icon: 'roundRect',
|
||||
},
|
||||
],
|
||||
selected: {
|
||||
'Total bid boost': true,
|
||||
},
|
||||
show: !this.widget,
|
||||
},
|
||||
yAxis: data.length === 0 ? undefined : [
|
||||
{
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: 'rgb(110, 112, 121)',
|
||||
formatter: (val) => {
|
||||
if (val >= 100_000) {
|
||||
return `${(val / 100_000_000).toFixed(3)} BTC`;
|
||||
} else {
|
||||
return `${val} sats`;
|
||||
}
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dotted',
|
||||
color: '#ffffff66',
|
||||
opacity: 0.25,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
position: 'right',
|
||||
axisLabel: {
|
||||
color: 'rgb(110, 112, 121)',
|
||||
formatter: function(val) {
|
||||
return `${val}`;
|
||||
}.bind(this)
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
series: data.length === 0 ? undefined : [
|
||||
{
|
||||
legendHoverLink: false,
|
||||
zlevel: 1,
|
||||
name: 'Total bid boost',
|
||||
data: data.map(h => {
|
||||
return [h.timestamp * 1000, h.sumBidBoost, h.avgHeight]
|
||||
}),
|
||||
stack: 'Total',
|
||||
type: 'bar',
|
||||
barWidth: '90%',
|
||||
large: true,
|
||||
},
|
||||
],
|
||||
dataZoom: (this.widget || data.length === 0 )? undefined : [{
|
||||
type: 'inside',
|
||||
realtime: true,
|
||||
zoomLock: true,
|
||||
maxSpan: 100,
|
||||
minSpan: 5,
|
||||
moveOnMouseMove: false,
|
||||
}, {
|
||||
showDetail: false,
|
||||
show: true,
|
||||
type: 'slider',
|
||||
brushSelect: false,
|
||||
realtime: true,
|
||||
left: 20,
|
||||
right: 15,
|
||||
selectedDataBackground: {
|
||||
lineStyle: {
|
||||
color: '#fff',
|
||||
opacity: 0.45,
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0,
|
||||
}
|
||||
},
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
onChartInit(ec) {
|
||||
this.chartInstance = ec;
|
||||
}
|
||||
|
||||
isMobile() {
|
||||
return (window.innerWidth <= 767.98);
|
||||
}
|
||||
|
||||
onSaveChart() {
|
||||
// @ts-ignore
|
||||
const prevBottom = this.chartOptions.grid.bottom;
|
||||
const now = new Date();
|
||||
// @ts-ignore
|
||||
this.chartOptions.grid.bottom = 40;
|
||||
this.chartOptions.backgroundColor = '#11131f';
|
||||
this.chartInstance.setOption(this.chartOptions);
|
||||
download(this.chartInstance.getDataURL({
|
||||
pixelRatio: 2,
|
||||
excludeComponents: ['dataZoom'],
|
||||
}), `acceleration-fees-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
|
||||
// @ts-ignore
|
||||
this.chartOptions.grid.bottom = prevBottom;
|
||||
this.chartOptions.backgroundColor = 'none';
|
||||
this.chartInstance.setOption(this.chartOptions);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.statsSubscription) {
|
||||
this.statsSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<div class="stats-wrapper" *ngIf="accelerationStats$ | async as stats; else loading">
|
||||
<div class="stats-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="accelerator.requests">Requests</h5>
|
||||
<div class="card-text">
|
||||
<div>{{ stats.totalRequested }}</div>
|
||||
<div class="symbol" i18n="accelerator.total-accelerated">accelerated</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="accelerator.total-boost">Total Bid Boost</h5>
|
||||
<div class="card-text">
|
||||
<div>{{ stats.totalBidBoost / 100_000_000 | amountShortener: 4 }} <span class="symbol" i18n="shared.btc|BTC">BTC</span></div>
|
||||
<span class="fiat">
|
||||
<app-fiat [value]="stats.totalBidBoost"></app-fiat>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="accelerator.success-rate">Success Rate</h5>
|
||||
<div class="card-text">
|
||||
<div>{{ stats.successRate.toFixed(2) }} %</div>
|
||||
<div class="symbol" i18n="accelerator.mined-next-block">mined</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loading>
|
||||
<div class="stats-container loading-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="accelerator.requests">Requests</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="accelerator.total-boost">Total Bid Boost</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="accelerator.success-rate">Success Rate</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,88 @@
|
||||
.card-title {
|
||||
color: #4a68b9;
|
||||
font-size: 10px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 1rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
font-size: 22px;
|
||||
span {
|
||||
font-size: 11px;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
display: inline-flex;
|
||||
}
|
||||
.green-color {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@media (min-width: 376px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
.item {
|
||||
max-width: 150px;
|
||||
margin: 0;
|
||||
width: -webkit-fill-available;
|
||||
@media (min-width: 376px) {
|
||||
margin: 0 auto 0px;
|
||||
}
|
||||
&:first-child{
|
||||
display: none;
|
||||
@media (min-width: 485px) {
|
||||
display: block;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.card-text span {
|
||||
color: #ffffff66;
|
||||
font-size: 12px;
|
||||
top: 0px;
|
||||
}
|
||||
.fee-text{
|
||||
border-bottom: 1px solid #ffffff1c;
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
line-height: 1.45;
|
||||
padding: 0px 2px;
|
||||
}
|
||||
.fiat {
|
||||
display: block;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container{
|
||||
min-height: 76px;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
display: block;
|
||||
&:first-child {
|
||||
max-width: 90px;
|
||||
margin: 15px auto 3px;
|
||||
}
|
||||
&:last-child {
|
||||
margin: 10px auto 3px;
|
||||
max-width: 55px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
||||
|
||||
export type AccelerationStats = {
|
||||
totalRequested: number;
|
||||
totalBidBoost: number;
|
||||
successRate: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-acceleration-stats',
|
||||
templateUrl: './acceleration-stats.component.html',
|
||||
styleUrls: ['./acceleration-stats.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AccelerationStatsComponent implements OnInit {
|
||||
accelerationStats$: Observable<AccelerationStats>;
|
||||
|
||||
constructor(
|
||||
private servicesApiService: ServicesApiServices
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.accelerationStats$ = this.servicesApiService.getAccelerationStats$();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<div class="container-lg widget-container" [class.widget]="widget" [class.full-height]="!widget">
|
||||
<h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Accelerations</h1>
|
||||
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="acceleration-list" *ngIf="accelerationList$ | async as accelerations">
|
||||
<table *ngIf="!accelerations || accelerations.length; else noData" class="table table-borderless table-fixed">
|
||||
<thead>
|
||||
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||
<ng-container *ngIf="pending">
|
||||
<th class="fee-rate text-right" i18n="transaction.fee|Transaction fee">Fee Rate</th>
|
||||
<th class="bid text-right" i18n="transaction.fee|Transaction fee">Acceleration Bid</th>
|
||||
<th class="time text-right" i18n="accelerator.block">Requested</th>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!pending">
|
||||
<th class="fee text-right" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th>
|
||||
<th class="block text-right" i18n="accelerator.block">Block</th>
|
||||
<th class="status text-right" i18n="transaction.status|Transaction Status">Status</th>
|
||||
<th class="date text-right" i18n="" *ngIf="!this.widget">Requested</th>
|
||||
</ng-container>
|
||||
</thead>
|
||||
<tbody *ngIf="accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
<tr *ngFor="let acceleration of accelerations; let i= index;">
|
||||
<td class="txid text-left">
|
||||
<a [routerLink]="['/tx' | relativeUrl, acceleration.txid]">
|
||||
<app-truncate [text]="acceleration.txid" [lastChars]="5"></app-truncate>
|
||||
</a>
|
||||
</td>
|
||||
<ng-container *ngIf="pending">
|
||||
<td class="fee-rate text-right">
|
||||
<app-fee-rate [fee]="acceleration.effectiveFee" [weight]="acceleration.effectiveVsize * 4"></app-fee-rate>
|
||||
</td>
|
||||
<td class="bid text-right">
|
||||
{{ (acceleration.feeDelta) | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
|
||||
</td>
|
||||
<td class="time text-right">
|
||||
<app-time kind="since" [time]="acceleration.added" [fastRender]="true"></app-time>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!pending">
|
||||
<td *ngIf="acceleration.feePaid" class="fee text-right">
|
||||
{{ (acceleration.boost) | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
|
||||
</td>
|
||||
<td *ngIf="!acceleration.feePaid" class="fee text-right">
|
||||
~
|
||||
</td>
|
||||
<td class="block text-right">
|
||||
<a [routerLink]="['/block' | relativeUrl, acceleration.blockHeight]">{{ acceleration.blockHeight }}</a>
|
||||
</td>
|
||||
<td class="status text-right">
|
||||
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span>
|
||||
<span *ngIf="acceleration.status === 'mined'" class="badge badge-info" i18n="transaction.rbf.mined">Mined</span>
|
||||
<span *ngIf="acceleration.status === 'completed'" class="badge badge-success" i18n="">Completed</span>
|
||||
<span *ngIf="acceleration.status === 'failed'" class="badge badge-danger" i18n="accelerator.canceled">Canceled</span>
|
||||
</td>
|
||||
<td class="date text-right" *ngIf="!this.widget">
|
||||
<app-time kind="since" [time]="acceleration.added" [fastRender]="true"></app-time>
|
||||
</td>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</tbody>
|
||||
<ng-template #skeleton>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of skeletonLines">
|
||||
<td class="txid text-left">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td class="fee text-right">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td class="fee-delta text-right">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td class="status text-right">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</ng-template>
|
||||
</table>
|
||||
|
||||
<ngb-pagination *ngIf="!widget" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
|
||||
[collectionSize]="this.accelerationCount" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page"
|
||||
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
|
||||
</ngb-pagination>
|
||||
|
||||
<ng-template [ngIf]="!widget">
|
||||
<div class="clearfix"></div>
|
||||
<br>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<ng-template #noData>
|
||||
<div class="no-data">
|
||||
<span *ngIf="pending" i18n="accelerations.no-accelerations">There are no active accelerations</span>
|
||||
<span *ngIf="!pending" i18n="accelerations.no-accelerations">There are no recent accelerations</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,186 @@
|
||||
.spinner-border {
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
margin-top: 13px;
|
||||
}
|
||||
|
||||
.container-xl {
|
||||
max-width: 1400px;
|
||||
}
|
||||
.container-xl.widget {
|
||||
padding-left: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
.container-xl.legacy {
|
||||
max-width: 1140px;
|
||||
}
|
||||
.container-xl.widget-container {
|
||||
min-height: 335px;
|
||||
@media (max-width: 767px) {
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.acceleration-list {
|
||||
min-height: 295px;
|
||||
@media (max-width: 767px) {
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
tr, td, th {
|
||||
border: 0px;
|
||||
padding-top: 0.65rem !important;
|
||||
padding-bottom: 0.8rem !important;
|
||||
|
||||
.difference {
|
||||
margin-left: 0.5em;
|
||||
|
||||
&.positive {
|
||||
color: rgb(66, 183, 71);
|
||||
}
|
||||
&.negative {
|
||||
color: rgb(183, 66, 66);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.clear-link {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.progress {
|
||||
background-color: #2d3348;
|
||||
}
|
||||
|
||||
.txid {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.fee {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.block {
|
||||
width: 15%;
|
||||
@media (max-width: 700px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
width: 13%;
|
||||
}
|
||||
|
||||
.date {
|
||||
width: 20%;
|
||||
@media (max-width: 600px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.widget {
|
||||
.txid {
|
||||
width: 30%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 30%;
|
||||
}
|
||||
|
||||
.fee-rate {
|
||||
width: 20%;
|
||||
text-align: end !important;
|
||||
@media (max-width: 975px) and (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 410px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bid {
|
||||
text-align: end !important;
|
||||
width: 30%;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.time {
|
||||
width: 25%;
|
||||
@media (max-width: 600px) {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 1200px) and (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fee {
|
||||
width: 30%;
|
||||
text-align: end !important;
|
||||
}
|
||||
|
||||
.block {
|
||||
width: 20%;
|
||||
@media (max-width: 1200px) and (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
width: 20%
|
||||
}
|
||||
}
|
||||
|
||||
/* Tooltip text */
|
||||
.tooltip-custom {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tooltip-custom .tooltiptext {
|
||||
visibility: hidden;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 5px 0;
|
||||
border-radius: 6px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: -40px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Show the tooltip text when you mouse over the tooltip container */
|
||||
.tooltip-custom:hover .tooltiptext {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.scriptmessage {
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
max-width: 50vw;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
display: flex;
|
||||
height: 280px;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@media (max-width: 767px) {
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core';
|
||||
import { combineLatest, BehaviorSubject, Observable, catchError, of, switchMap, tap } from 'rxjs';
|
||||
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-accelerations-list',
|
||||
templateUrl: './accelerations-list.component.html',
|
||||
styleUrls: ['./accelerations-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AccelerationsListComponent implements OnInit {
|
||||
@Input() widget: boolean = false;
|
||||
@Input() pending: boolean = false;
|
||||
@Input() accelerations$: Observable<Acceleration[]>;
|
||||
|
||||
accelerationList$: Observable<Acceleration[]> = undefined;
|
||||
|
||||
isLoading = true;
|
||||
paginationMaxSize: number;
|
||||
page = 1;
|
||||
accelerationCount: number;
|
||||
maxSize = window.innerWidth <= 767.98 ? 3 : 5;
|
||||
skeletonLines: number[] = [];
|
||||
pageSubject: BehaviorSubject<number> = new BehaviorSubject(this.page);
|
||||
|
||||
constructor(
|
||||
private servicesApiService: ServicesApiServices,
|
||||
private websocketService: WebsocketService,
|
||||
public stateService: StateService,
|
||||
private cd: ChangeDetectorRef,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.widget) {
|
||||
this.websocketService.want(['blocks']);
|
||||
}
|
||||
|
||||
this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
|
||||
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
|
||||
|
||||
this.accelerationList$ = this.pageSubject.pipe(
|
||||
switchMap((page) => {
|
||||
const accelerationObservable$ = this.accelerations$ || (this.pending ? this.servicesApiService.getAccelerations$() : this.servicesApiService.getAccelerationHistoryObserveResponse$({ timeframe: '1y', page: page }));
|
||||
return accelerationObservable$.pipe(
|
||||
switchMap(response => {
|
||||
let accelerations = response;
|
||||
if (response.body) {
|
||||
accelerations = response.body;
|
||||
this.accelerationCount = parseInt(response.headers.get('x-total-count'), 10);
|
||||
}
|
||||
if (this.pending) {
|
||||
for (const acceleration of accelerations) {
|
||||
acceleration.status = acceleration.status || 'accelerating';
|
||||
}
|
||||
}
|
||||
for (const acc of accelerations) {
|
||||
acc.boost = acc.feePaid - acc.baseFee - acc.vsizeFee;
|
||||
}
|
||||
if (this.widget) {
|
||||
return of(accelerations.slice(0, 6));
|
||||
} else {
|
||||
return of(accelerations);
|
||||
}
|
||||
}),
|
||||
catchError((err) => {
|
||||
this.isLoading = false;
|
||||
return of([]);
|
||||
}),
|
||||
tap(() => {
|
||||
this.isLoading = false;
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
pageChange(page: number): void {
|
||||
this.pageSubject.next(page);
|
||||
}
|
||||
|
||||
trackByBlock(index: number, block: BlockExtended): number {
|
||||
return block.height;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<app-indexing-progress></app-indexing-progress>
|
||||
|
||||
<div class="container-xl dashboard-container">
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2">
|
||||
|
||||
<!-- pending stats -->
|
||||
<div class="col">
|
||||
<div class="main-title">
|
||||
<span [attr.data-cy]="'pending-accelerations'" i18n="accelerator.pending-accelerations">Active Accelerations</span>
|
||||
</div>
|
||||
<div class="card-wrapper">
|
||||
<div class="card">
|
||||
<div class="card-body more-padding">
|
||||
<app-pending-stats [accelerations$]="pendingAccelerations$"></app-pending-stats>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 1m stats -->
|
||||
<div class="col">
|
||||
<div class="main-title">
|
||||
<span [attr.data-cy]="'acceleration-stats'" i18n="accelerator.acceleration-stats">Acceleration stats</span>
|
||||
<span style="font-size: xx-small" i18n="mining.3-months">(3 months)</span>
|
||||
</div>
|
||||
<div class="card-wrapper">
|
||||
<div class="card">
|
||||
<div class="card-body more-padding">
|
||||
<app-acceleration-stats></app-acceleration-stats>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Next block visualization -->
|
||||
<div class="col" style="margin-bottom: 1.47rem">
|
||||
<div class="card">
|
||||
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
|
||||
<a class="title-link" href="" [routerLink]="['/mempool-block/0' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.mempool-goggles-accelerations">Mempool Goggles: Accelerations</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
<div class="mempool-block-wrapper">
|
||||
<app-mempool-block-overview [index]="0" [overrideColors]="getAcceleratorColor"></app-mempool-block-overview>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- acceleration fees graph -->
|
||||
<div class="col" style="margin-bottom: 1.47rem">
|
||||
<div class="card graph-card">
|
||||
<div class="card-body pl-2 pr-2">
|
||||
<h5 class="card-title" i18n="acceleration.total-bid-boost">Total Bid Boost</h5>
|
||||
<div class="mempool-graph">
|
||||
<app-acceleration-fees-graph
|
||||
[height]="graphHeight"
|
||||
[attr.data-cy]="'acceleration-fees'"
|
||||
[widget]=true
|
||||
></app-acceleration-fees-graph>
|
||||
</div>
|
||||
<div class="mt-1"><a [attr.data-cy]="'acceleration-fees-view-more'" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Avg block fees graph -->
|
||||
<!-- <div class="col" style="margin-bottom: 1.47rem">
|
||||
<div class="card">
|
||||
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
|
||||
<app-block-fee-rates-graph [attr.data-cy]="'hashrate-graph'" [widget]="true"></app-block-fee-rates-graph>
|
||||
<div class="mt-1"><a [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" fragment="1m" i18n="dashboard.view-more">View more »</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- Recent Accelerations List -->
|
||||
<div class="col">
|
||||
<div class="card list-card">
|
||||
<div class="card-body">
|
||||
<div class="title-link">
|
||||
<h5 class="card-title d-inline" i18n="accelerator.pending-accelerations">Active Accelerations</h5>
|
||||
</div>
|
||||
<app-accelerations-list [attr.data-cy]="'pending-accelerations'" [widget]=true [pending]=true [accelerations$]="pendingAccelerations$"></app-accelerations-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirmed List -->
|
||||
<div class="col">
|
||||
<div class="card list-card">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/acceleration/list' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.recent-accelerations">Recent Accelerations</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
<app-accelerations-list [attr.data-cy]="'recent-accelerations'" [widget]=true [accelerations$]="minedAccelerations$"></app-accelerations-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,175 @@
|
||||
.dashboard-container {
|
||||
text-align: center;
|
||||
margin-top: 0.5rem;
|
||||
.col {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #1d1f31;
|
||||
}
|
||||
|
||||
.graph-card {
|
||||
height: 100%;
|
||||
@media (min-width: 992px) {
|
||||
height: 385px;
|
||||
}
|
||||
}
|
||||
|
||||
.mempool-graph {
|
||||
height: 295px;
|
||||
@media (min-width: 768px) {
|
||||
height: 325px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
height: 409px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1rem;
|
||||
color: #4a68b9;
|
||||
}
|
||||
.card-title > a {
|
||||
color: #4a68b9;
|
||||
}
|
||||
|
||||
.card-body.pool-ranking {
|
||||
padding: 1.25rem 0.25rem 0.75rem 0.25rem;
|
||||
}
|
||||
.card-text {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
#blockchain-container {
|
||||
position: relative;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
#blockchain-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.fade-border {
|
||||
-webkit-mask-image: linear-gradient(to right, transparent 0%, black 10%, black 80%, transparent 100%)
|
||||
}
|
||||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.more-padding {
|
||||
padding: 24px 20px !important;
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
.card {
|
||||
height: auto !important;
|
||||
}
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex: inherit;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
padding: 22px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
display: block;
|
||||
&:first-child {
|
||||
max-width: 90px;
|
||||
margin: 15px auto 3px;
|
||||
}
|
||||
&:last-child {
|
||||
margin: 10px auto 3px;
|
||||
max-width: 55px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-text {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.lastest-blocks-table {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
tr, td, th {
|
||||
border: 0px;
|
||||
padding-top: 0.65rem !important;
|
||||
padding-bottom: 0.8rem !important;
|
||||
}
|
||||
.table-cell-height {
|
||||
width: 25%;
|
||||
}
|
||||
.table-cell-fee {
|
||||
width: 25%;
|
||||
text-align: right;
|
||||
}
|
||||
.table-cell-pool {
|
||||
text-align: left;
|
||||
width: 30%;
|
||||
|
||||
@media (max-width: 875px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pool-name {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
.table-cell-acceleration-count {
|
||||
text-align: right;
|
||||
width: 20%;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
@media (min-width: 768px) {
|
||||
height: 420px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
height: 510px;
|
||||
}
|
||||
}
|
||||
.list-card {
|
||||
height: 410px;
|
||||
@media (max-width: 767px) {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.mempool-block-wrapper {
|
||||
max-height: 430px;
|
||||
max-width: 430px;
|
||||
margin: auto;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
max-height: 344px;
|
||||
max-width: 344px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
max-height: 430px;
|
||||
max-width: 430px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { ChangeDetectionStrategy, Component, HostListener, OnInit } from '@angular/core';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
import { Observable, catchError, combineLatest, distinctUntilChanged, interval, map, of, share, startWith, switchMap, tap } from 'rxjs';
|
||||
import { Color } from '../../block-overview-graph/sprite-types';
|
||||
import { hexToColor } from '../../block-overview-graph/utils';
|
||||
import TxView from '../../block-overview-graph/tx-view';
|
||||
import { feeLevels, mempoolFeeColors } from '../../../app.constants';
|
||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
||||
|
||||
const acceleratedColor: Color = hexToColor('8F5FF6');
|
||||
const normalColors = mempoolFeeColors.map(hex => hexToColor(hex + '5F'));
|
||||
|
||||
interface AccelerationBlock extends BlockExtended {
|
||||
accelerationCount: number,
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-accelerator-dashboard',
|
||||
templateUrl: './accelerator-dashboard.component.html',
|
||||
styleUrls: ['./accelerator-dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AcceleratorDashboardComponent implements OnInit {
|
||||
blocks$: Observable<AccelerationBlock[]>;
|
||||
accelerations$: Observable<Acceleration[]>;
|
||||
pendingAccelerations$: Observable<Acceleration[]>;
|
||||
minedAccelerations$: Observable<Acceleration[]>;
|
||||
loadingBlocks: boolean = true;
|
||||
|
||||
graphHeight: number = 300;
|
||||
|
||||
constructor(
|
||||
private seoService: SeoService,
|
||||
private websocketService: WebsocketService,
|
||||
private serviceApiServices: ServicesApiServices,
|
||||
private stateService: StateService,
|
||||
) {
|
||||
this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Accelerator Dashboard`);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.onResize();
|
||||
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
|
||||
|
||||
this.pendingAccelerations$ = interval(30000).pipe(
|
||||
startWith(true),
|
||||
switchMap(() => {
|
||||
return this.serviceApiServices.getAccelerations$().pipe(
|
||||
catchError(() => {
|
||||
return of([]);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
share(),
|
||||
);
|
||||
|
||||
this.accelerations$ = this.stateService.chainTip$.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap(() => {
|
||||
return this.serviceApiServices.getAccelerationHistory$({ timeframe: '3m', page: 1, pageLength: 100}).pipe(
|
||||
catchError(() => {
|
||||
return of([]);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
share(),
|
||||
);
|
||||
|
||||
this.minedAccelerations$ = this.accelerations$.pipe(
|
||||
map(accelerations => {
|
||||
return accelerations.filter(acc => ['mined', 'completed'].includes(acc.status));
|
||||
})
|
||||
);
|
||||
|
||||
this.blocks$ = combineLatest([
|
||||
this.accelerations$,
|
||||
this.stateService.blocks$.pipe(
|
||||
switchMap((blocks) => {
|
||||
if (this.stateService.env.MINING_DASHBOARD === true) {
|
||||
for (const block of blocks) {
|
||||
// @ts-ignore: Need to add an extra field for the template
|
||||
block.extras.pool.logo = `/resources/mining-pools/` +
|
||||
block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
|
||||
}
|
||||
}
|
||||
return of(blocks as AccelerationBlock[]);
|
||||
}),
|
||||
tap(() => {
|
||||
this.loadingBlocks = false;
|
||||
})
|
||||
)
|
||||
]).pipe(
|
||||
switchMap(([accelerations, blocks]) => {
|
||||
const blockMap = {};
|
||||
for (const block of blocks) {
|
||||
blockMap[block.id] = block;
|
||||
}
|
||||
const accelerationsByBlock: { [ hash: string ]: Acceleration[] } = {};
|
||||
for (const acceleration of accelerations) {
|
||||
if (['mined', 'completed'].includes(acceleration.status) && acceleration.pools.includes(blockMap[acceleration.blockHash]?.extras.pool.id)) {
|
||||
if (!accelerationsByBlock[acceleration.blockHash]) {
|
||||
accelerationsByBlock[acceleration.blockHash] = [];
|
||||
}
|
||||
accelerationsByBlock[acceleration.blockHash].push(acceleration);
|
||||
}
|
||||
}
|
||||
return of(blocks.slice(0, 6).map(block => {
|
||||
block.accelerationCount = (accelerationsByBlock[block.id] || []).length;
|
||||
return block;
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getAcceleratorColor(tx: TxView): Color {
|
||||
if (tx.status === 'accelerated' || tx.acc) {
|
||||
return acceleratedColor;
|
||||
} else {
|
||||
const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate
|
||||
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1;
|
||||
return normalColors[feeLevelIndex] || normalColors[mempoolFeeColors.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(): void {
|
||||
if (window.innerWidth >= 992) {
|
||||
this.graphHeight = 380;
|
||||
} else if (window.innerWidth >= 768) {
|
||||
this.graphHeight = 300;
|
||||
} else {
|
||||
this.graphHeight = 270;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<div class="stats-wrapper" *ngIf="accelerationStats$ | async as stats; else loading">
|
||||
<div class="stats-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="accelerator.requests">Requests</h5>
|
||||
<div class="card-text">
|
||||
<div>{{ stats.count }}</div>
|
||||
<div class="symbol" i18n="accelerator.total-pending">pending</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="accelerator.average-max-bid">Avg Max Bid</h5>
|
||||
<div class="card-text">
|
||||
<div>{{ stats.avgFeeDelta / 100_000_000 | amountShortener: 4 }} <span class="symbol" i18n="shared.btc|BTC">BTC</span></div>
|
||||
<span class="fiat">
|
||||
<app-fiat [value]="stats.avgFeeDelta"></app-fiat>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="accelerator.total-vsize">Total Vsize</h5>
|
||||
<div class="card-text">
|
||||
<div [innerHTML]="'‎' + (stats.totalVsize * 4 | vbytes: 2)"></div>
|
||||
<div class="symbol">{{ (stats.totalVsize / 1_000_000 * 100).toFixed(2) }}% <span i18n="accelerator.percent-of-next-block"> of next block</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loading>
|
||||
<div class="stats-container loading-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="accelerator.requests">Requests</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="accelerator.average-max-bid">Avg Max Bid</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="accelerator.total-vsize">Total Vsize</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,88 @@
|
||||
.card-title {
|
||||
color: #4a68b9;
|
||||
font-size: 10px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 1rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
font-size: 22px;
|
||||
span {
|
||||
font-size: 11px;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
display: inline-flex;
|
||||
}
|
||||
.green-color {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@media (min-width: 376px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
.item {
|
||||
max-width: 150px;
|
||||
margin: 0;
|
||||
width: -webkit-fill-available;
|
||||
@media (min-width: 376px) {
|
||||
margin: 0 auto 0px;
|
||||
}
|
||||
&:first-child{
|
||||
display: none;
|
||||
@media (min-width: 485px) {
|
||||
display: block;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.card-text span {
|
||||
color: #ffffff66;
|
||||
font-size: 12px;
|
||||
top: 0px;
|
||||
}
|
||||
.fee-text{
|
||||
border-bottom: 1px solid #ffffff1c;
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
line-height: 1.45;
|
||||
padding: 0px 2px;
|
||||
}
|
||||
.fiat {
|
||||
display: block;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container{
|
||||
min-height: 76px;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
display: block;
|
||||
&:first-child {
|
||||
max-width: 90px;
|
||||
margin: 15px auto 3px;
|
||||
}
|
||||
&:last-child {
|
||||
margin: 10px auto 3px;
|
||||
max-width: 55px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { Acceleration } from '../../../interfaces/node-api.interface';
|
||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pending-stats',
|
||||
templateUrl: './pending-stats.component.html',
|
||||
styleUrls: ['./pending-stats.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PendingStatsComponent implements OnInit {
|
||||
@Input() accelerations$: Observable<Acceleration[]>;
|
||||
public accelerationStats$: Observable<any>;
|
||||
|
||||
constructor(
|
||||
private servicesApiService: ServicesApiServices,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.accelerationStats$ = (this.accelerations$ || this.servicesApiService.getAccelerations$()).pipe(
|
||||
switchMap(accelerations => {
|
||||
let totalAccelerations = 0;
|
||||
let totalFeeDelta = 0;
|
||||
let totalVsize = 0;
|
||||
for (const acceleration of accelerations) {
|
||||
totalAccelerations++;
|
||||
totalFeeDelta += acceleration.feeDelta || 0;
|
||||
totalVsize += acceleration.effectiveVsize || 0;
|
||||
}
|
||||
return of({
|
||||
count: totalAccelerations,
|
||||
totalFeeDelta,
|
||||
avgFeeDelta: totalAccelerations ? totalFeeDelta / totalAccelerations : 0,
|
||||
totalVsize,
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ export class AddressLabelsComponent implements OnChanges {
|
||||
|
||||
handleVin() {
|
||||
if (this.vin.inner_witnessscript_asm) {
|
||||
if (this.vin.inner_witnessscript_asm.indexOf('OP_DEPTH OP_PUSHNUM_12 OP_EQUAL OP_IF OP_PUSHNUM_11') === 0) {
|
||||
if (this.vin.inner_witnessscript_asm.indexOf('OP_DEPTH OP_PUSHNUM_12 OP_EQUAL OP_IF OP_PUSHNUM_11') === 0 || this.vin.inner_witnessscript_asm.indexOf('OP_PUSHNUM_15 OP_CHECKMULTISIG OP_IFDUP OP_NOTIF OP_PUSHBYTES_2') === 1259) {
|
||||
if (this.vin.witness.length > 11) {
|
||||
this.label = 'Liquid Peg Out';
|
||||
} else {
|
||||
|
||||
@@ -69,7 +69,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
||||
this.addressString = this.addressString.toLowerCase();
|
||||
}
|
||||
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'} ${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`);
|
||||
|
||||
return (this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
|
||||
? this.electrsApiService.getPubKeyAddress$(this.addressString)
|
||||
|
||||
@@ -31,8 +31,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
addressLoadingStatus$: Observable<number>;
|
||||
addressInfo: null | AddressInformation = null;
|
||||
|
||||
totalConfirmedTxCount = 0;
|
||||
loadedConfirmedTxCount = 0;
|
||||
fullyLoaded = false;
|
||||
txCount = 0;
|
||||
received = 0;
|
||||
sent = 0;
|
||||
@@ -66,7 +65,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
switchMap((params: ParamMap) => {
|
||||
this.error = undefined;
|
||||
this.isLoadingAddress = true;
|
||||
this.loadedConfirmedTxCount = 0;
|
||||
this.fullyLoaded = false;
|
||||
this.address = null;
|
||||
this.isLoadingTransactions = true;
|
||||
this.transactions = null;
|
||||
@@ -105,7 +104,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
.pipe(
|
||||
filter((address) => !!address),
|
||||
tap((address: Address) => {
|
||||
if ((this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') && /^([m-zA-HJ-NP-Z1-9]{26,35}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[a-km-zA-HJ-NP-Z1-9]{80})$/.test(address.address)) {
|
||||
if ((this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') && /^([a-zA-HJ-NP-Z1-9]{26,35}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[a-km-zA-HJ-NP-Z1-9]{80})$/.test(address.address)) {
|
||||
this.apiService.validateAddress$(address.address)
|
||||
.subscribe((addressInfo) => {
|
||||
this.addressInfo = addressInfo;
|
||||
@@ -128,7 +127,6 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.tempTransactions = transactions;
|
||||
if (transactions.length) {
|
||||
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
|
||||
this.loadedConfirmedTxCount += transactions.filter((tx) => tx.status.confirmed).length;
|
||||
}
|
||||
|
||||
const fetchTxs: string[] = [];
|
||||
@@ -174,6 +172,11 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.addTransaction(tx);
|
||||
});
|
||||
|
||||
this.stateService.mempoolRemovedTransactions$
|
||||
.subscribe(tx => {
|
||||
this.removeTransaction(tx);
|
||||
});
|
||||
|
||||
this.stateService.blockTransactions$
|
||||
.subscribe((transaction) => {
|
||||
const tx = this.transactions.find((t) => t.txid === transaction.txid);
|
||||
@@ -186,8 +189,6 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.audioService.playSound('magic');
|
||||
}
|
||||
}
|
||||
this.totalConfirmedTxCount++;
|
||||
this.loadedConfirmedTxCount++;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -222,17 +223,44 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
return true;
|
||||
}
|
||||
|
||||
removeTransaction(transaction: Transaction): boolean {
|
||||
const index = this.transactions.findIndex(((tx) => tx.txid === transaction.txid));
|
||||
if (index === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.transactions.splice(index, 1);
|
||||
this.transactions = this.transactions.slice();
|
||||
this.txCount--;
|
||||
|
||||
transaction.vin.forEach((vin) => {
|
||||
if (vin?.prevout?.scriptpubkey_address === this.address.address) {
|
||||
this.sent -= vin.prevout.value;
|
||||
}
|
||||
});
|
||||
transaction.vout.forEach((vout) => {
|
||||
if (vout?.scriptpubkey_address === this.address.address) {
|
||||
this.received -= vout.value;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
if (this.isLoadingTransactions || !this.totalConfirmedTxCount || this.loadedConfirmedTxCount >= this.totalConfirmedTxCount) {
|
||||
if (this.isLoadingTransactions || this.fullyLoaded) {
|
||||
return;
|
||||
}
|
||||
this.isLoadingTransactions = true;
|
||||
this.retryLoadMore = false;
|
||||
this.electrsApiService.getAddressTransactions$(this.address.address, this.lastTransactionTxId)
|
||||
.subscribe((transactions: Transaction[]) => {
|
||||
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
|
||||
this.loadedConfirmedTxCount += transactions.length;
|
||||
this.transactions = this.transactions.concat(transactions);
|
||||
if (transactions && transactions.length) {
|
||||
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
|
||||
this.transactions = this.transactions.concat(transactions);
|
||||
} else {
|
||||
this.fullyLoaded = true;
|
||||
}
|
||||
this.isLoadingTransactions = false;
|
||||
},
|
||||
(error) => {
|
||||
@@ -249,7 +277,6 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.received = this.address.chain_stats.funded_txo_sum + this.address.mempool_stats.funded_txo_sum;
|
||||
this.sent = this.address.chain_stats.spent_txo_sum + this.address.mempool_stats.spent_txo_sum;
|
||||
this.txCount = this.address.chain_stats.tx_count + this.address.mempool_stats.tx_count;
|
||||
this.totalConfirmedTxCount = this.address.chain_stats.tx_count;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</ng-template>
|
||||
<ng-template #default>
|
||||
‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis / 100000000 | number : digitsInfo }}
|
||||
<span class="symbol"><ng-template [ngIf]="network === 'liquid'">L-</ng-template>
|
||||
<span class="symbol"><ng-template [ngIf]="network === 'liquid' && !forceBtc">L-</ng-template>
|
||||
<ng-template [ngIf]="network === 'liquidtestnet'">tL-</ng-template>
|
||||
<ng-template [ngIf]="network === 'testnet'">t</ng-template>
|
||||
<ng-template [ngIf]="network === 'signet'">s</ng-template>BTC</span>
|
||||
|
||||
@@ -23,6 +23,7 @@ export class AmountComponent implements OnInit, OnDestroy {
|
||||
@Input() noFiat = false;
|
||||
@Input() addPlus = false;
|
||||
@Input() blockConversion: Price;
|
||||
@Input() forceBtc: boolean = false;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<ng-template #title>
|
||||
<div class="main-title">
|
||||
<h2>{{ group.name }}</h2>
|
||||
<h2>{{ group['group'].name }}</h2>
|
||||
<div class="sub-title" i18n>Group of {{ group.assets.length | number }} assets</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<header>
|
||||
<header class="sticky-header">
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
||||
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
|
||||
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
.sticky-header {
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
li.nav-item.active {
|
||||
background-color: #653b9c;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<app-indexing-progress></app-indexing-progress>
|
||||
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
|
||||
|
||||
<div class="full-container">
|
||||
<div class="card-header mb-0 mb-md-4">
|
||||
<div [class.full-container]="!widget">
|
||||
<div *ngIf="!widget" class="card-header mb-0 mb-md-4">
|
||||
<div class="d-flex d-md-block align-items-baseline">
|
||||
<span i18n="mining.block-fee-rates">Block Fee Rates</span>
|
||||
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
|
||||
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
||||
<div class="btn-group btn-group-toggle" name="radioBasic" [class]="{'disabled': isLoading}">
|
||||
@@ -45,11 +45,45 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div *ngIf="widget">
|
||||
<div class="block-fee-rates">
|
||||
<div class="item" *ngIf="(hrStatsObservable$ | async) as stats; else loadingHrStats">
|
||||
<h5 class="card-title" i18n="mining.avg-block-fee-24h">Avg Block Fee (24h)</h5>
|
||||
<p class="card-text">
|
||||
<app-fee-rate [fee]="stats.avgMedianRate"></app-fee-rate>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item" *ngIf="(statsObservable$ | async) as stats; else loadingStats">
|
||||
<h5 class="card-title" i18n="mining.avg-block-fee-1m">Avg Block Fee (1m)</h5>
|
||||
<p class="card-text">
|
||||
<app-fee-rate [fee]="stats.avgMedianRate"></app-fee-rate>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div [class.chart]="!widget" [class.chart-widget]="widget" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingHrStats>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.avg-block-fee-24h">Avg Block Fee (24h)</h5>
|
||||
<p class="card-text">
|
||||
<span class="skeleton-loader skeleton-loader-big"></span>
|
||||
</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template #loadingStats>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.avg-block-fee-1m">Avg Block Fee (1m)</h5>
|
||||
<p class="card-text">
|
||||
<span class="skeleton-loader skeleton-loader-big"></span>
|
||||
</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -57,7 +57,54 @@
|
||||
.chart-widget {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 270px;
|
||||
max-height: 238px;
|
||||
}
|
||||
|
||||
.block-fee-rates {
|
||||
min-height: 56px;
|
||||
display: block;
|
||||
@media (min-width: 485px) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
h5 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.item {
|
||||
width: 50%;
|
||||
display: inline-block;
|
||||
margin: 0px auto 20px;
|
||||
&:nth-child(2) {
|
||||
order: 2;
|
||||
@media (min-width: 485px) {
|
||||
order: 3;
|
||||
}
|
||||
}
|
||||
&:nth-child(3) {
|
||||
order: 3;
|
||||
@media (min-width: 485px) {
|
||||
order: 2;
|
||||
display: block;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.card-title {
|
||||
font-size: 1rem;
|
||||
color: #4a68b9;
|
||||
}
|
||||
.card-text {
|
||||
font-size: 18px;
|
||||
span {
|
||||
color: #ffffff66;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.formRadioGroup {
|
||||
@@ -85,6 +132,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
display: block;
|
||||
max-width: 80px;
|
||||
margin: 15px auto 3px;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { Observable, Subscription, combineLatest } from 'rxjs';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { Observable, combineLatest, of } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
@@ -29,6 +29,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BlockFeeRatesGraphComponent implements OnInit {
|
||||
@Input() widget = false;
|
||||
@Input() right: number | string = 45;
|
||||
@Input() left: number | string = 75;
|
||||
|
||||
@@ -40,6 +41,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
||||
renderer: 'svg',
|
||||
};
|
||||
|
||||
hrStatsObservable$: Observable<any>;
|
||||
statsObservable$: Observable<any>;
|
||||
isLoading = true;
|
||||
formatNumber = formatNumber;
|
||||
@@ -57,39 +59,61 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
||||
private router: Router,
|
||||
private zone: NgZone,
|
||||
private route: ActivatedRoute,
|
||||
private cd: ChangeDetectorRef,
|
||||
) {
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
|
||||
this.radioGroupForm.controls.dateSpan.setValue('1y');
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`:@@ed8e33059967f554ff06b4f5b6049c465b92d9b3:Block Fee Rates`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-fee-rates:See Bitcoin feerates visualized over time, including minimum and maximum feerates per block along with feerates at various percentiles.`);
|
||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
|
||||
if (this.widget) {
|
||||
this.miningWindowPreference = '1m';
|
||||
} else {
|
||||
this.seoService.setTitle($localize`:@@ed8e33059967f554ff06b4f5b6049c465b92d9b3:Block Fee Rates`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-fee-rates:See Bitcoin feerates visualized over time, including minimum and maximum feerates per block along with feerates at various percentiles.`);
|
||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
|
||||
}
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
||||
|
||||
this.route
|
||||
.fragment
|
||||
.subscribe((fragment) => {
|
||||
if (['24h', '3d', '1w', '1m', '3m', '6m', '1y', '2y', '3y', 'all'].indexOf(fragment) > -1) {
|
||||
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
|
||||
}
|
||||
});
|
||||
if (!this.widget) {
|
||||
this.route
|
||||
.fragment
|
||||
.subscribe((fragment) => {
|
||||
if (['24h', '3d', '1w', '1m', '3m', '6m', '1y', '2y', '3y', 'all'].indexOf(fragment) > -1) {
|
||||
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.hrStatsObservable$ = combineLatest([
|
||||
this.apiService.getHistoricalBlockFeeRates$('24h'),
|
||||
this.stateService.rateUnits$
|
||||
]).pipe(
|
||||
map(([response, rateUnits]) => {
|
||||
return {
|
||||
blockCount: parseInt(response.headers.get('x-total-count'), 10),
|
||||
avgMedianRate: response.body.length ? response.body.reduce((acc, rate) => acc + rate.avgFee_50, 0) / response.body.length : 0,
|
||||
};
|
||||
}),
|
||||
share(),
|
||||
);
|
||||
|
||||
this.statsObservable$ = combineLatest([
|
||||
this.radioGroupForm.get('dateSpan').valueChanges.pipe(startWith(this.radioGroupForm.controls.dateSpan.value)),
|
||||
this.widget ? of(this.miningWindowPreference) : this.radioGroupForm.get('dateSpan').valueChanges.pipe(startWith(this.radioGroupForm.controls.dateSpan.value)),
|
||||
this.stateService.rateUnits$
|
||||
]).pipe(
|
||||
switchMap(([timespan, rateUnits]) => {
|
||||
this.storageService.setValue('miningWindowPreference', timespan);
|
||||
if (!this.widget) {
|
||||
this.storageService.setValue('miningWindowPreference', timespan);
|
||||
}
|
||||
this.timespan = timespan;
|
||||
this.isLoading = true;
|
||||
return this.apiService.getHistoricalBlockFeeRates$(timespan)
|
||||
.pipe(
|
||||
tap((response) => {
|
||||
// Group by percentile
|
||||
const seriesData = {
|
||||
const seriesData = this.widget ? { 'Median': [] } : {
|
||||
'Min': [],
|
||||
'10th': [],
|
||||
'25th': [],
|
||||
@@ -100,13 +124,17 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
||||
};
|
||||
for (const rate of response.body) {
|
||||
const timestamp = rate.timestamp * 1000;
|
||||
seriesData['Min'].push([timestamp, rate.avgFee_0, rate.avgHeight]);
|
||||
seriesData['10th'].push([timestamp, rate.avgFee_10, rate.avgHeight]);
|
||||
seriesData['25th'].push([timestamp, rate.avgFee_25, rate.avgHeight]);
|
||||
seriesData['Median'].push([timestamp, rate.avgFee_50, rate.avgHeight]);
|
||||
seriesData['75th'].push([timestamp, rate.avgFee_75, rate.avgHeight]);
|
||||
seriesData['90th'].push([timestamp, rate.avgFee_90, rate.avgHeight]);
|
||||
seriesData['Max'].push([timestamp, rate.avgFee_100, rate.avgHeight]);
|
||||
if (this.widget) {
|
||||
seriesData['Median'].push([timestamp, rate.avgFee_50, rate.avgHeight]);
|
||||
} else {
|
||||
seriesData['Min'].push([timestamp, rate.avgFee_0, rate.avgHeight]);
|
||||
seriesData['10th'].push([timestamp, rate.avgFee_10, rate.avgHeight]);
|
||||
seriesData['25th'].push([timestamp, rate.avgFee_25, rate.avgHeight]);
|
||||
seriesData['Median'].push([timestamp, rate.avgFee_50, rate.avgHeight]);
|
||||
seriesData['75th'].push([timestamp, rate.avgFee_75, rate.avgHeight]);
|
||||
seriesData['90th'].push([timestamp, rate.avgFee_90, rate.avgHeight]);
|
||||
seriesData['Max'].push([timestamp, rate.avgFee_100, rate.avgHeight]);
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare chart
|
||||
@@ -135,15 +163,42 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.widget) {
|
||||
let maResolution = 30;
|
||||
const medianMa = [];
|
||||
for (let i = maResolution - 1; i < seriesData['Median'].length; ++i) {
|
||||
let avg = 0;
|
||||
for (let y = maResolution - 1; y >= 0; --y) {
|
||||
avg += seriesData['Median'][i - y][1];
|
||||
}
|
||||
avg /= maResolution;
|
||||
medianMa.push([seriesData['Median'][i][0], avg, seriesData['Median'][i][2]]);
|
||||
}
|
||||
series.push({
|
||||
zlevel: 1,
|
||||
name: 'Moving average',
|
||||
data: medianMa,
|
||||
type: 'line',
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.prepareChartOptions({
|
||||
legends: legends,
|
||||
series: series
|
||||
}, rateUnits === 'wu');
|
||||
|
||||
this.isLoading = false;
|
||||
this.cd.markForCheck();
|
||||
}),
|
||||
map((response) => {
|
||||
return {
|
||||
blockCount: parseInt(response.headers.get('x-total-count'), 10),
|
||||
avgMedianRate: response.body.length ? response.body.reduce((acc, rate) => acc + rate.avgFee_50, 0) / response.body.length : 0,
|
||||
};
|
||||
}),
|
||||
);
|
||||
@@ -154,13 +209,19 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
||||
|
||||
prepareChartOptions(data, weightMode) {
|
||||
this.chartOptions = {
|
||||
color: ['#D81B60', '#8E24AA', '#1E88E5', '#7CB342', '#FDD835', '#6D4C41', '#546E7A'],
|
||||
color: this.widget ? ['#6b6b6b', new graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||
{ offset: 0, color: '#F4511E' },
|
||||
{ offset: 0.25, color: '#FB8C00' },
|
||||
{ offset: 0.5, color: '#FFB300' },
|
||||
{ offset: 0.75, color: '#FDD835' },
|
||||
{ offset: 1, color: '#7CB342' }
|
||||
])] : ['#D81B60', '#8E24AA', '#1E88E5', '#7CB342', '#FDD835', '#6D4C41', '#546E7A'],
|
||||
animation: false,
|
||||
grid: {
|
||||
right: this.right,
|
||||
left: this.left,
|
||||
bottom: 80,
|
||||
top: this.isMobile() ? 10 : 50,
|
||||
bottom: this.widget ? 30 : 80,
|
||||
top: this.widget ? 20 : (this.isMobile() ? 10 : 50),
|
||||
},
|
||||
tooltip: {
|
||||
show: !this.isMobile(),
|
||||
@@ -184,9 +245,9 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
||||
|
||||
for (const rate of data.reverse()) {
|
||||
if (weightMode) {
|
||||
tooltip += `${rate.marker} ${rate.seriesName}: ${rate.data[1] / 4} sats/WU<br>`;
|
||||
tooltip += `${rate.marker} ${rate.seriesName}: ${(rate.data[1] / 4).toFixed(2)} sats/WU<br>`;
|
||||
} else {
|
||||
tooltip += `${rate.marker} ${rate.seriesName}: ${rate.data[1]} sats/vByte<br>`;
|
||||
tooltip += `${rate.marker} ${rate.seriesName}: ${rate.data[1].toFixed(2)} sats/vByte<br>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +262,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
||||
},
|
||||
xAxis: data.series.length === 0 ? undefined :
|
||||
{
|
||||
name: formatterXAxisLabel(this.locale, this.timespan),
|
||||
name: this.widget ? undefined : formatterXAxisLabel(this.locale, this.timespan),
|
||||
nameLocation: 'middle',
|
||||
nameTextStyle: {
|
||||
padding: [10, 0, 0, 0],
|
||||
@@ -218,7 +279,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
||||
padding: [0, 5],
|
||||
},
|
||||
},
|
||||
legend: (data.series.length === 0) ? undefined : {
|
||||
legend: (this.widget || data.series.length === 0) ? undefined : {
|
||||
padding: [10, 75],
|
||||
data: data.legends,
|
||||
selected: JSON.parse(this.storageService.getValue('fee_rates_legend')) ?? {
|
||||
@@ -256,7 +317,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
||||
max: (val) => this.timespan === 'all' ? Math.min(val.max, 5000) : undefined,
|
||||
},
|
||||
series: data.series,
|
||||
dataZoom: [{
|
||||
dataZoom: this.widget ? null : [{
|
||||
type: 'inside',
|
||||
realtime: true,
|
||||
zoomLock: true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { echarts, EChartsOption } from '../../graphs/echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
@@ -123,11 +123,11 @@ export class BlockFeesGraphComponent implements OnInit {
|
||||
this.chartOptions = {
|
||||
title: title,
|
||||
color: [
|
||||
new graphic.LinearGradient(0, 0, 0, 1, [
|
||||
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#FDD835' },
|
||||
{ offset: 1, color: '#FB8C00' },
|
||||
]),
|
||||
new graphic.LinearGradient(0, 0, 0, 1, [
|
||||
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#C0CA33' },
|
||||
{ offset: 1, color: '#1B5E20' },
|
||||
]),
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<div class="block-filters" [class.filters-active]="activeFilters.length > 0" [class.any-mode]="filterMode === 'or'" [class.menu-open]="menuOpen" [class.small]="cssWidth < 500" [class.vsmall]="cssWidth < 400" [class.tiny]="cssWidth < 200">
|
||||
<a *ngIf="menuOpen" [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-do-mempool-goggles-work" class="info-badges" i18n-ngbTooltip="Mempool Goggles tooltip" ngbTooltip="select filter categories to highlight matching transactions">
|
||||
<span class="badge badge-pill badge-warning beta" i18n="beta">beta</span>
|
||||
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="lg"></fa-icon>
|
||||
</a>
|
||||
<div class="filter-bar">
|
||||
<button class="menu-toggle" (click)="menuOpen = !menuOpen" title="Mempool Goggles">
|
||||
<app-svg-images name="goggles" width="100%" height="100%"></app-svg-images>
|
||||
</button>
|
||||
<div class="active-tags">
|
||||
<ng-container *ngFor="let filter of activeFilters;">
|
||||
<button class="btn filter-tag selected" (click)="toggleFilter(filter)">{{ filters[filter].label }}</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-menu" *ngIf="menuOpen && cssWidth > 280">
|
||||
<h5>Match</h5>
|
||||
<div class="btn-group btn-group-toggle">
|
||||
<label class="btn btn-xs blue mode-toggle" [class.active]="filterMode === 'and'">
|
||||
<input type="radio" [value]="'all'" fragment="all" (click)="setFilterMode('and')">All
|
||||
</label>
|
||||
<label class="btn btn-xs green mode-toggle" [class.active]="filterMode === 'or'">
|
||||
<input type="radio" [value]="'any'" fragment="any" (click)="setFilterMode('or')">Any
|
||||
</label>
|
||||
</div>
|
||||
<ng-container *ngFor="let group of filterGroups;">
|
||||
<h5>{{ group.label }}</h5>
|
||||
<div class="filter-group">
|
||||
<ng-container *ngFor="let filter of group.filters;">
|
||||
<button *ngIf="!disabledFilters[filter.key]" class="btn filter-tag" [class.selected]="filterFlags[filter.key]" (click)="toggleFilter(filter.key)">{{ filter.label }}</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="filter-menu" *ngIf="menuOpen && cssWidth <= 280">
|
||||
<ng-container *ngFor="let group of filterGroups;">
|
||||
<ng-container *ngFor="let filter of group.filters;">
|
||||
<button *ngIf="filter.important" class="btn filter-tag" [class.selected]="filterFlags[filter.key]" (click)="toggleFilter(filter.key)">{{ filter.label }}</button>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,190 @@
|
||||
.block-filters {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 1em;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
|
||||
.filter-bar, .active-tags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.active-tags {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 0.25em;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.info-badges {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
float: right;
|
||||
|
||||
&:hover, &:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
padding: 0px 1px;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
background: none;
|
||||
border: solid 2px white;
|
||||
border-radius: 0.35em;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.filter-menu {
|
||||
h5 {
|
||||
font-size: 0.8rem;
|
||||
color: white;
|
||||
margin: 0;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 0.25em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.filter-tag {
|
||||
font-size: 0.9em;
|
||||
background: #181b2daf;
|
||||
border: solid 1px #105fb0;
|
||||
color: white;
|
||||
border-radius: 0.2rem;
|
||||
padding: 0.2em 0.5em;
|
||||
transition: background-color 300ms;
|
||||
margin-right: 0.25em;
|
||||
pointer-events: all;
|
||||
|
||||
&.selected {
|
||||
background-color: #105fb0;
|
||||
}
|
||||
}
|
||||
|
||||
&.any-mode {
|
||||
.filter-tag {
|
||||
border: solid 1px #1a9436;
|
||||
&.selected {
|
||||
background-color: #1a9436;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
font-size: 0.9em;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.mode-toggle {
|
||||
padding: 0.2em 0.5em;
|
||||
pointer-events: all;
|
||||
line-height: 1.5;
|
||||
background: #181b2daf;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 0.2rem;
|
||||
border-bottom-left-radius: 0.2rem;
|
||||
}
|
||||
&:last-child {
|
||||
border-top-right-radius: 0.2rem;
|
||||
border-bottom-right-radius: 0.2rem;
|
||||
}
|
||||
|
||||
&.blue {
|
||||
border: solid 1px #105fb0;
|
||||
&.active {
|
||||
background: #105fb0;
|
||||
}
|
||||
}
|
||||
&.green {
|
||||
border: solid 1px #1a9436;
|
||||
&.active {
|
||||
background: #1a9436;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.block-overview-graph:hover) &, &:hover, &:active {
|
||||
.menu-toggle {
|
||||
opacity: 0.5;
|
||||
background: #181b2d;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background: #181b2d7f;
|
||||
}
|
||||
}
|
||||
|
||||
&.menu-open, &.filters-active {
|
||||
.menu-toggle {
|
||||
opacity: 1;
|
||||
background: none;
|
||||
|
||||
&:hover {
|
||||
background: #181b2d7f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.menu-open, &.filters-active {
|
||||
.menu-toggle {
|
||||
opacity: 1;
|
||||
background: none;
|
||||
|
||||
&:hover {
|
||||
background: #181b2d7f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.menu-open {
|
||||
pointer-events: all;
|
||||
background: #181b2d7f;
|
||||
}
|
||||
|
||||
&.small {
|
||||
.filter-tag {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
|
||||
&.vsmall {
|
||||
.filter-menu {
|
||||
margin-top: 0.25em;
|
||||
h5 {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.filter-tag {
|
||||
font-size: 0.7em;
|
||||
}
|
||||
.mode-toggle {
|
||||
font-size: 0.7em;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&.tiny {
|
||||
.filter-tag {
|
||||
font-size: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ActiveFilter, FilterGroups, FilterMode, TransactionFilters } from '../../shared/filters.utils';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-block-filters',
|
||||
templateUrl: './block-filters.component.html',
|
||||
styleUrls: ['./block-filters.component.scss'],
|
||||
})
|
||||
export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() cssWidth: number = 800;
|
||||
@Input() excludeFilters: string[] = [];
|
||||
@Output() onFilterChanged: EventEmitter<ActiveFilter | null> = new EventEmitter();
|
||||
|
||||
filterSubscription: Subscription;
|
||||
|
||||
filters = TransactionFilters;
|
||||
filterGroups = FilterGroups;
|
||||
disabledFilters: { [key: string]: boolean } = {};
|
||||
activeFilters: string[] = [];
|
||||
filterFlags: { [key: string]: boolean } = {};
|
||||
filterMode: FilterMode = 'and';
|
||||
menuOpen: boolean = false;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private cd: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.filterSubscription = this.stateService.activeGoggles$.subscribe((active: ActiveFilter) => {
|
||||
this.filterMode = active.mode;
|
||||
for (const key of Object.keys(this.filterFlags)) {
|
||||
this.filterFlags[key] = false;
|
||||
}
|
||||
for (const key of active.filters) {
|
||||
this.filterFlags[key] = !this.disabledFilters[key];
|
||||
}
|
||||
this.activeFilters = [...active.filters.filter(key => !this.disabledFilters[key])];
|
||||
this.onFilterChanged.emit({ mode: active.mode, filters: this.activeFilters });
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.cssWidth) {
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
if (changes.excludeFilters) {
|
||||
this.disabledFilters = {};
|
||||
this.excludeFilters.forEach(filter => {
|
||||
this.disabledFilters[filter] = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setFilterMode(mode): void {
|
||||
this.filterMode = mode;
|
||||
this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters });
|
||||
this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters] });
|
||||
}
|
||||
|
||||
toggleFilter(key): void {
|
||||
const filter = this.filters[key];
|
||||
this.filterFlags[key] = !this.filterFlags[key];
|
||||
if (this.filterFlags[key]) {
|
||||
// remove any other flags in the same toggle group
|
||||
if (filter.toggle) {
|
||||
this.activeFilters.forEach(f => {
|
||||
if (this.filters[f].toggle === filter.toggle) {
|
||||
this.filterFlags[f] = false;
|
||||
}
|
||||
});
|
||||
this.activeFilters = this.activeFilters.filter(f => this.filters[f].toggle !== filter.toggle);
|
||||
}
|
||||
// add new active filter
|
||||
this.activeFilters.push(key);
|
||||
} else {
|
||||
// remove active filter
|
||||
this.activeFilters = this.activeFilters.filter(f => f != key);
|
||||
}
|
||||
const booleanFlags = this.getBooleanFlags();
|
||||
this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters });
|
||||
this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters] });
|
||||
}
|
||||
|
||||
getBooleanFlags(): bigint | null {
|
||||
let flags = 0n;
|
||||
for (const key of Object.keys(this.filterFlags)) {
|
||||
if (this.filterFlags[key]) {
|
||||
flags |= this.filters[key].flag;
|
||||
}
|
||||
}
|
||||
return flags || null;
|
||||
}
|
||||
|
||||
@HostListener('document:click', ['$event'])
|
||||
onClick(event): boolean {
|
||||
// click away from menu
|
||||
if (!event.target.closest('button') && !event.target.closest('label')) {
|
||||
this.menuOpen = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.filterSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { EChartsOption } from '../../graphs/echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
|
||||
@@ -13,5 +13,9 @@
|
||||
[auditEnabled]="auditHighlighting"
|
||||
[blockConversion]="blockConversion"
|
||||
></app-block-overview-tooltip>
|
||||
<app-block-filters *ngIf="webGlEnabled && showFilters && filtersAvailable" [excludeFilters]="excludeFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters>
|
||||
<div *ngIf="!webGlEnabled" class="placeholder">
|
||||
<span i18n="webgl-disabled">Your browser does not support this feature.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,19 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
grid-column: 1/-1;
|
||||
|
||||
.placeholder {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-align {
|
||||
|
||||
@@ -4,10 +4,25 @@ import { FastVertexArray } from './fast-vertex-array';
|
||||
import BlockScene from './block-scene';
|
||||
import TxSprite from './tx-sprite';
|
||||
import TxView from './tx-view';
|
||||
import { Position } from './sprite-types';
|
||||
import { Color, Position } from './sprite-types';
|
||||
import { Price } from '../../services/price.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { defaultColorFunction, setOpacity, defaultFeeColors, defaultAuditFeeColors, defaultMarginalFeeColors, defaultAuditColors } from './utils';
|
||||
import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils';
|
||||
import { detectWebGL } from '../../shared/graphs.utils';
|
||||
|
||||
const unmatchedOpacity = 0.2;
|
||||
const unmatchedFeeColors = defaultFeeColors.map(c => setOpacity(c, unmatchedOpacity));
|
||||
const unmatchedAuditFeeColors = defaultAuditFeeColors.map(c => setOpacity(c, unmatchedOpacity));
|
||||
const unmatchedMarginalFeeColors = defaultMarginalFeeColors.map(c => setOpacity(c, unmatchedOpacity));
|
||||
const unmatchedAuditColors = {
|
||||
censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity),
|
||||
missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity),
|
||||
added: setOpacity(defaultAuditColors.added, unmatchedOpacity),
|
||||
selected: setOpacity(defaultAuditColors.selected, unmatchedOpacity),
|
||||
accelerated: setOpacity(defaultAuditColors.accelerated, unmatchedOpacity),
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-block-overview-graph',
|
||||
@@ -20,11 +35,18 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
@Input() blockLimit: number;
|
||||
@Input() orientation = 'left';
|
||||
@Input() flip = true;
|
||||
@Input() animationDuration: number = 1000;
|
||||
@Input() animationOffset: number | null = null;
|
||||
@Input() disableSpinner = false;
|
||||
@Input() mirrorTxid: string | void;
|
||||
@Input() unavailable: boolean = false;
|
||||
@Input() auditHighlighting: boolean = false;
|
||||
@Input() showFilters: boolean = false;
|
||||
@Input() excludeFilters: string[] = [];
|
||||
@Input() filterFlags: bigint | null = null;
|
||||
@Input() filterMode: FilterMode = 'and';
|
||||
@Input() blockConversion: Price;
|
||||
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
|
||||
@Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
|
||||
@Output() txHoverEvent = new EventEmitter<string>();
|
||||
@Output() readyEvent = new EventEmitter();
|
||||
@@ -53,12 +75,17 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
|
||||
searchText: string;
|
||||
searchSubscription: Subscription;
|
||||
filtersAvailable: boolean = true;
|
||||
activeFilterFlags: bigint | null = null;
|
||||
|
||||
webGlEnabled = true;
|
||||
|
||||
constructor(
|
||||
readonly ngZone: NgZone,
|
||||
readonly elRef: ElementRef,
|
||||
private stateService: StateService,
|
||||
) {
|
||||
this.webGlEnabled = detectWebGL();
|
||||
this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
|
||||
this.searchSubscription = this.stateService.searchText$.subscribe((text) => {
|
||||
this.searchText = text;
|
||||
@@ -89,6 +116,25 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
if (changes.auditHighlighting) {
|
||||
this.setHighlightingEnabled(this.auditHighlighting);
|
||||
}
|
||||
if (changes.overrideColor && this.scene) {
|
||||
this.scene.setColorFunction(this.overrideColors);
|
||||
}
|
||||
if ((changes.filterFlags || changes.showFilters || changes.filterMode)) {
|
||||
this.setFilterFlags();
|
||||
}
|
||||
}
|
||||
|
||||
setFilterFlags(goggle?: ActiveFilter): void {
|
||||
this.filterMode = goggle?.mode || this.filterMode;
|
||||
this.activeFilterFlags = goggle?.filters ? toFlags(goggle.filters) : this.filterFlags;
|
||||
if (this.scene) {
|
||||
if (this.activeFilterFlags != null && this.filtersAvailable) {
|
||||
this.scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags));
|
||||
} else {
|
||||
this.scene.setColorFunction(this.overrideColors);
|
||||
}
|
||||
}
|
||||
this.start();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -117,6 +163,11 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
|
||||
// initialize the scene without any entry transition
|
||||
setup(transactions: TransactionStripped[]): void {
|
||||
const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
|
||||
if (filtersAvailable !== this.filtersAvailable) {
|
||||
this.setFilterFlags();
|
||||
}
|
||||
this.filtersAvailable = filtersAvailable;
|
||||
if (this.scene) {
|
||||
this.scene.setup(transactions);
|
||||
this.readyNextFrame = true;
|
||||
@@ -141,9 +192,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
}
|
||||
}
|
||||
|
||||
replace(transactions: TransactionStripped[], direction: string, sort: boolean = true): void {
|
||||
replace(transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void {
|
||||
if (this.scene) {
|
||||
this.scene.replace(transactions || [], direction, sort);
|
||||
this.scene.replace(transactions || [], direction, sort, startTime);
|
||||
this.start();
|
||||
this.updateSearchHighlight();
|
||||
}
|
||||
@@ -226,7 +277,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
} else {
|
||||
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
|
||||
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
|
||||
highlighting: this.auditHighlighting });
|
||||
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset,
|
||||
colorFunction: this.getColorFunction() });
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
@@ -367,6 +419,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
onPointerMove(event) {
|
||||
if (event.target === this.canvas.nativeElement) {
|
||||
this.setPreviewTx(event.offsetX, event.offsetY, false);
|
||||
} else {
|
||||
this.onPointerLeave(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,17 +510,45 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
}
|
||||
|
||||
onTxClick(cssX: number, cssY: number, keyModifier: boolean = false) {
|
||||
const x = cssX * window.devicePixelRatio;
|
||||
const y = cssY * window.devicePixelRatio;
|
||||
const selected = this.scene.getTxAt({ x, y });
|
||||
if (selected && selected.txid) {
|
||||
this.txClickEvent.emit({ tx: selected, keyModifier });
|
||||
if (this.scene) {
|
||||
const x = cssX * window.devicePixelRatio;
|
||||
const y = cssY * window.devicePixelRatio;
|
||||
const selected = this.scene.getTxAt({ x, y });
|
||||
if (selected && selected.txid) {
|
||||
this.txClickEvent.emit({ tx: selected, keyModifier });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTxHover(hoverId: string) {
|
||||
this.txHoverEvent.emit(hoverId);
|
||||
}
|
||||
|
||||
getColorFunction(): ((tx: TxView) => Color) {
|
||||
if (this.filterFlags) {
|
||||
return this.getFilterColorFunction(this.filterFlags);
|
||||
} else if (this.activeFilterFlags) {
|
||||
return this.getFilterColorFunction(this.activeFilterFlags);
|
||||
} else {
|
||||
return this.overrideColors;
|
||||
}
|
||||
}
|
||||
|
||||
getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) {
|
||||
return (tx: TxView) => {
|
||||
if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) {
|
||||
return defaultColorFunction(tx);
|
||||
} else {
|
||||
return defaultColorFunction(
|
||||
tx,
|
||||
unmatchedFeeColors,
|
||||
unmatchedAuditFeeColors,
|
||||
unmatchedMarginalFeeColors,
|
||||
unmatchedAuditColors
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// WebGL shader attributes
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { FastVertexArray } from './fast-vertex-array';
|
||||
import TxView from './tx-view';
|
||||
import { TransactionStripped } from '../../interfaces/websocket.interface';
|
||||
import { Position, Square, ViewUpdateParams } from './sprite-types';
|
||||
import { Color, Position, Square, ViewUpdateParams } from './sprite-types';
|
||||
import { defaultColorFunction } from './utils';
|
||||
|
||||
export default class BlockScene {
|
||||
scene: { count: number, offset: { x: number, y: number}};
|
||||
vertexArray: FastVertexArray;
|
||||
txs: { [key: string]: TxView };
|
||||
getColor: ((tx: TxView) => Color) = defaultColorFunction;
|
||||
orientation: string;
|
||||
flip: boolean;
|
||||
animationDuration: number = 900;
|
||||
configAnimationOffset: number | null;
|
||||
animationOffset: number;
|
||||
highlightingEnabled: boolean;
|
||||
filterFlags: bigint | null = 0b00000100_00000000_00000000_00000000n;
|
||||
width: number;
|
||||
height: number;
|
||||
gridWidth: number;
|
||||
@@ -23,11 +29,11 @@ export default class BlockScene {
|
||||
animateUntil = 0;
|
||||
dirty: boolean;
|
||||
|
||||
constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
|
||||
{ width: number, height: number, resolution: number, blockLimit: number,
|
||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
|
||||
constructor({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting, colorFunction }:
|
||||
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
|
||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null }
|
||||
) {
|
||||
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting });
|
||||
this.init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting, colorFunction });
|
||||
}
|
||||
|
||||
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
|
||||
@@ -36,6 +42,7 @@ export default class BlockScene {
|
||||
this.gridSize = this.width / this.gridWidth;
|
||||
this.unitPadding = Math.max(1, Math.floor(this.gridSize / 5));
|
||||
this.unitWidth = this.gridSize - (this.unitPadding * 2);
|
||||
this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset;
|
||||
|
||||
this.dirty = true;
|
||||
if (this.initialised && this.scene) {
|
||||
@@ -59,6 +66,14 @@ export default class BlockScene {
|
||||
}
|
||||
}
|
||||
|
||||
setColorFunction(colorFunction: ((tx: TxView) => Color) | null): void {
|
||||
this.getColor = colorFunction || defaultColorFunction;
|
||||
this.dirty = true;
|
||||
if (this.initialised && this.scene) {
|
||||
this.updateColors(performance.now(), 50);
|
||||
}
|
||||
}
|
||||
|
||||
// Destroy the current layout and clean up graphics sprites without any exit animation
|
||||
destroy(): void {
|
||||
Object.values(this.txs).forEach(tx => tx.destroy());
|
||||
@@ -82,7 +97,7 @@ export default class BlockScene {
|
||||
this.applyTxUpdate(txView, {
|
||||
display: {
|
||||
position: txView.screenPosition,
|
||||
color: txView.getColor()
|
||||
color: this.getColor(txView)
|
||||
},
|
||||
duration: 0
|
||||
});
|
||||
@@ -90,8 +105,8 @@ export default class BlockScene {
|
||||
}
|
||||
|
||||
// Animate new block entering scene
|
||||
enter(txs: TransactionStripped[], direction) {
|
||||
this.replace(txs, direction);
|
||||
enter(txs: TransactionStripped[], direction, startTime?: number) {
|
||||
this.replace(txs, direction, false, startTime);
|
||||
}
|
||||
|
||||
// Animate block leaving scene
|
||||
@@ -108,8 +123,7 @@ export default class BlockScene {
|
||||
}
|
||||
|
||||
// Reset layout and replace with new set of transactions
|
||||
replace(txs: TransactionStripped[], direction: string = 'left', sort: boolean = true): void {
|
||||
const startTime = performance.now();
|
||||
replace(txs: TransactionStripped[], direction: string = 'left', sort: boolean = true, startTime: number = performance.now()): void {
|
||||
const nextIds = {};
|
||||
const remove = [];
|
||||
txs.forEach(tx => {
|
||||
@@ -133,7 +147,7 @@ export default class BlockScene {
|
||||
removed.forEach(tx => {
|
||||
tx.destroy();
|
||||
});
|
||||
}, 1000);
|
||||
}, (startTime - performance.now()) + this.animationDuration + 1000);
|
||||
|
||||
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
|
||||
|
||||
@@ -147,7 +161,7 @@ export default class BlockScene {
|
||||
});
|
||||
}
|
||||
|
||||
this.updateAll(startTime, 200, direction);
|
||||
this.updateAll(startTime, 50, direction);
|
||||
}
|
||||
|
||||
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
||||
@@ -214,14 +228,18 @@ export default class BlockScene {
|
||||
this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value));
|
||||
}
|
||||
|
||||
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
|
||||
{ width: number, height: number, resolution: number, blockLimit: number,
|
||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
|
||||
private init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting, colorFunction }:
|
||||
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
|
||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null }
|
||||
): void {
|
||||
this.animationDuration = animationDuration || 1000;
|
||||
this.configAnimationOffset = animationOffset;
|
||||
this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset;
|
||||
this.orientation = orientation;
|
||||
this.flip = flip;
|
||||
this.vertexArray = vertexArray;
|
||||
this.highlightingEnabled = highlighting;
|
||||
this.getColor = colorFunction || defaultColorFunction;
|
||||
|
||||
this.scene = {
|
||||
count: 0,
|
||||
@@ -248,6 +266,20 @@ export default class BlockScene {
|
||||
this.animateUntil = Math.max(this.animateUntil, tx.update(update));
|
||||
}
|
||||
|
||||
private updateTxColor(tx: TxView, startTime: number, delay: number, animate: boolean = true, duration?: number): void {
|
||||
if (tx.dirty || this.dirty) {
|
||||
const txColor = this.getColor(tx);
|
||||
this.applyTxUpdate(tx, {
|
||||
display: {
|
||||
color: txColor
|
||||
},
|
||||
duration: animate ? (duration || this.animationDuration) : 1,
|
||||
start: startTime,
|
||||
delay: animate ? delay : 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private updateTx(tx: TxView, startTime: number, delay: number, direction: string = 'left', animate: boolean = true): void {
|
||||
if (tx.dirty || this.dirty) {
|
||||
this.saveGridToScreenPosition(tx);
|
||||
@@ -255,14 +287,28 @@ export default class BlockScene {
|
||||
}
|
||||
}
|
||||
|
||||
private updateColor(tx: TxView, startTime: number, delay: number, animate: boolean = true, duration: number = 500): void {
|
||||
if (tx.dirty || this.dirty) {
|
||||
const txColor = this.getColor(tx);
|
||||
this.applyTxUpdate(tx, {
|
||||
display: {
|
||||
color: txColor,
|
||||
},
|
||||
start: startTime,
|
||||
delay,
|
||||
duration: animate ? duration : 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private setTxOnScreen(tx: TxView, startTime: number, delay: number = 50, direction: string = 'left', animate: boolean = true): void {
|
||||
if (!tx.initialised) {
|
||||
const txColor = tx.getColor();
|
||||
const txColor = this.getColor(tx);
|
||||
this.applyTxUpdate(tx, {
|
||||
display: {
|
||||
position: {
|
||||
x: tx.screenPosition.x + (direction === 'right' ? -this.width : (direction === 'left' ? this.width : 0)) * 1.4,
|
||||
y: tx.screenPosition.y + (direction === 'up' ? -this.height : (direction === 'down' ? this.height : 0)) * 1.4,
|
||||
x: tx.screenPosition.x + (direction === 'right' ? -this.width - this.animationOffset : (direction === 'left' ? this.width + this.animationOffset : 0)),
|
||||
y: tx.screenPosition.y + (direction === 'up' ? -this.height - this.animationOffset : (direction === 'down' ? this.height + this.animationOffset : 0)),
|
||||
s: tx.screenPosition.s
|
||||
},
|
||||
color: txColor,
|
||||
@@ -275,17 +321,17 @@ export default class BlockScene {
|
||||
position: tx.screenPosition,
|
||||
color: txColor
|
||||
},
|
||||
duration: animate ? 1000 : 1,
|
||||
duration: animate ? this.animationDuration : 1,
|
||||
start: startTime,
|
||||
delay: animate ? delay : 0,
|
||||
});
|
||||
} else {
|
||||
this.applyTxUpdate(tx, {
|
||||
display: {
|
||||
position: tx.screenPosition
|
||||
position: tx.screenPosition,
|
||||
},
|
||||
duration: animate ? 1000 : 0,
|
||||
minDuration: animate ? 500 : 0,
|
||||
duration: animate ? this.animationDuration : 0,
|
||||
minDuration: animate ? (this.animationDuration / 2) : 0,
|
||||
start: startTime,
|
||||
delay: animate ? delay : 0,
|
||||
adjust: animate
|
||||
@@ -315,6 +361,15 @@ export default class BlockScene {
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
private updateColors(startTime: number, delay: number = 50, animate: boolean = true, duration: number = 500): void {
|
||||
const ids = this.getTxList();
|
||||
startTime = startTime || performance.now();
|
||||
for (const id of ids) {
|
||||
this.updateColor(this.txs[id], startTime, delay, animate, duration);
|
||||
}
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
private remove(id: string, startTime: number, direction: string = 'left'): TxView | void {
|
||||
const tx = this.txs[id];
|
||||
if (tx) {
|
||||
@@ -322,11 +377,11 @@ export default class BlockScene {
|
||||
this.applyTxUpdate(tx, {
|
||||
display: {
|
||||
position: {
|
||||
x: tx.screenPosition.x + (direction === 'right' ? this.width : (direction === 'left' ? -this.width : 0)) * 1.4,
|
||||
y: tx.screenPosition.y + (direction === 'up' ? this.height : (direction === 'down' ? -this.height : 0)) * 1.4,
|
||||
x: tx.screenPosition.x + (direction === 'right' ? this.width + this.animationOffset : (direction === 'left' ? -this.width - this.animationOffset : 0)),
|
||||
y: tx.screenPosition.y + (direction === 'up' ? this.height + this.animationOffset : (direction === 'down' ? -this.height - this.animationOffset : 0)),
|
||||
}
|
||||
},
|
||||
duration: 1000,
|
||||
duration: this.animationDuration,
|
||||
start: startTime,
|
||||
delay: 50
|
||||
});
|
||||
@@ -851,4 +906,4 @@ class BlockLayout {
|
||||
|
||||
function feeRateDescending(a: TxView, b: TxView) {
|
||||
return b.feerate - a.feerate;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,15 @@
|
||||
import TxSprite from './tx-sprite';
|
||||
import { FastVertexArray } from './fast-vertex-array';
|
||||
import { TransactionStripped } from '../../interfaces/websocket.interface';
|
||||
import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types';
|
||||
import { feeLevels, mempoolFeeColors } from '../../app.constants';
|
||||
import { hexToColor } from './utils';
|
||||
import BlockScene from './block-scene';
|
||||
import { TransactionStripped } from '../../interfaces/node-api.interface';
|
||||
import { TransactionFlags } from '../../shared/filters.utils';
|
||||
|
||||
const hoverTransitionTime = 300;
|
||||
const defaultHoverColor = hexToColor('1bd8f4');
|
||||
const defaultHighlightColor = hexToColor('800080');
|
||||
|
||||
const feeColors = mempoolFeeColors.map(hexToColor);
|
||||
const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
|
||||
const marginalFeeColors = feeColors.map((color) => darken(desaturate(color, 0.8), 1.1));
|
||||
const auditColors = {
|
||||
censored: hexToColor('f344df'),
|
||||
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
|
||||
added: hexToColor('0099ff'),
|
||||
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
|
||||
accelerated: hexToColor('8F5FF6'),
|
||||
};
|
||||
|
||||
// convert from this class's update format to TxSprite's update format
|
||||
function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams {
|
||||
return {
|
||||
@@ -40,6 +30,7 @@ export default class TxView implements TransactionStripped {
|
||||
feerate: number;
|
||||
acc?: boolean;
|
||||
rate?: number;
|
||||
bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n;
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
||||
context?: 'projected' | 'actual';
|
||||
scene?: BlockScene;
|
||||
@@ -68,6 +59,7 @@ export default class TxView implements TransactionStripped {
|
||||
this.acc = tx.acc;
|
||||
this.rate = tx.rate;
|
||||
this.status = tx.status;
|
||||
this.bigintFlags = tx.flags ? (BigInt(tx.flags) | (this.acc ? TransactionFlags.acceleration : 0n)): 0n;
|
||||
this.initialised = false;
|
||||
this.vertexArray = scene.vertexArray;
|
||||
|
||||
@@ -195,77 +187,4 @@ export default class TxView implements TransactionStripped {
|
||||
this.dirty = false;
|
||||
return performance.now() + hoverTransitionTime;
|
||||
}
|
||||
|
||||
getColor(): Color {
|
||||
const rate = this.fee / this.vsize; // color by simple single-tx fee rate
|
||||
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1;
|
||||
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
|
||||
// Normal mode
|
||||
if (!this.scene?.highlightingEnabled) {
|
||||
if (this.acc) {
|
||||
return auditColors.accelerated;
|
||||
} else {
|
||||
return feeLevelColor;
|
||||
}
|
||||
return feeLevelColor;
|
||||
}
|
||||
// Block audit
|
||||
switch(this.status) {
|
||||
case 'censored':
|
||||
return auditColors.censored;
|
||||
case 'missing':
|
||||
case 'sigop':
|
||||
case 'rbf':
|
||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||
case 'fresh':
|
||||
case 'freshcpfp':
|
||||
return auditColors.missing;
|
||||
case 'added':
|
||||
return auditColors.added;
|
||||
case 'selected':
|
||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||
case 'accelerated':
|
||||
return auditColors.accelerated;
|
||||
case 'found':
|
||||
if (this.context === 'projected') {
|
||||
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
|
||||
} else {
|
||||
return feeLevelColor;
|
||||
}
|
||||
default:
|
||||
if (this.acc) {
|
||||
return auditColors.accelerated;
|
||||
} else {
|
||||
return feeLevelColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hexToColor(hex: string): Color {
|
||||
return {
|
||||
r: parseInt(hex.slice(0, 2), 16) / 255,
|
||||
g: parseInt(hex.slice(2, 4), 16) / 255,
|
||||
b: parseInt(hex.slice(4, 6), 16) / 255,
|
||||
a: 1
|
||||
};
|
||||
}
|
||||
|
||||
function desaturate(color: Color, amount: number): Color {
|
||||
const gray = (color.r + color.g + color.b) / 6;
|
||||
return {
|
||||
r: color.r + ((gray - color.r) * amount),
|
||||
g: color.g + ((gray - color.g) * amount),
|
||||
b: color.b + ((gray - color.b) * amount),
|
||||
a: color.a,
|
||||
};
|
||||
}
|
||||
|
||||
function darken(color: Color, amount: number): Color {
|
||||
return {
|
||||
r: color.r * amount,
|
||||
g: color.g * amount,
|
||||
b: color.b * amount,
|
||||
a: color.a,
|
||||
}
|
||||
}
|
||||
|
||||
101
frontend/src/app/components/block-overview-graph/utils.ts
Normal file
101
frontend/src/app/components/block-overview-graph/utils.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { feeLevels, mempoolFeeColors } from '../../app.constants';
|
||||
import { Color } from './sprite-types';
|
||||
import TxView from './tx-view';
|
||||
|
||||
export function hexToColor(hex: string): Color {
|
||||
return {
|
||||
r: parseInt(hex.slice(0, 2), 16) / 255,
|
||||
g: parseInt(hex.slice(2, 4), 16) / 255,
|
||||
b: parseInt(hex.slice(4, 6), 16) / 255,
|
||||
a: hex.length > 6 ? parseInt(hex.slice(6, 8), 16) / 255 : 1
|
||||
};
|
||||
}
|
||||
|
||||
export function desaturate(color: Color, amount: number): Color {
|
||||
const gray = (color.r + color.g + color.b) / 6;
|
||||
return {
|
||||
r: color.r + ((gray - color.r) * amount),
|
||||
g: color.g + ((gray - color.g) * amount),
|
||||
b: color.b + ((gray - color.b) * amount),
|
||||
a: color.a,
|
||||
};
|
||||
}
|
||||
|
||||
export function darken(color: Color, amount: number): Color {
|
||||
return {
|
||||
r: color.r * amount,
|
||||
g: color.g * amount,
|
||||
b: color.b * amount,
|
||||
a: color.a,
|
||||
};
|
||||
}
|
||||
|
||||
export function setOpacity(color: Color, opacity: number): Color {
|
||||
return {
|
||||
...color,
|
||||
a: opacity
|
||||
};
|
||||
}
|
||||
|
||||
// precomputed colors
|
||||
export const defaultFeeColors = mempoolFeeColors.map(hexToColor);
|
||||
export const defaultAuditFeeColors = defaultFeeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
|
||||
export const defaultMarginalFeeColors = defaultFeeColors.map((color) => darken(desaturate(color, 0.8), 1.1));
|
||||
export const defaultAuditColors = {
|
||||
censored: hexToColor('f344df'),
|
||||
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
|
||||
added: hexToColor('0099ff'),
|
||||
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
|
||||
accelerated: hexToColor('8F5FF6'),
|
||||
};
|
||||
|
||||
export function defaultColorFunction(
|
||||
tx: TxView,
|
||||
feeColors: Color[] = defaultFeeColors,
|
||||
auditFeeColors: Color[] = defaultAuditFeeColors,
|
||||
marginalFeeColors: Color[] = defaultMarginalFeeColors,
|
||||
auditColors: { [status: string]: Color } = defaultAuditColors
|
||||
): Color {
|
||||
const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate
|
||||
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1;
|
||||
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
|
||||
// Normal mode
|
||||
if (!tx.scene?.highlightingEnabled) {
|
||||
if (tx.acc) {
|
||||
return auditColors.accelerated;
|
||||
} else {
|
||||
return feeLevelColor;
|
||||
}
|
||||
return feeLevelColor;
|
||||
}
|
||||
// Block audit
|
||||
switch(tx.status) {
|
||||
case 'censored':
|
||||
return auditColors.censored;
|
||||
case 'missing':
|
||||
case 'sigop':
|
||||
case 'rbf':
|
||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||
case 'fresh':
|
||||
case 'freshcpfp':
|
||||
return auditColors.missing;
|
||||
case 'added':
|
||||
return auditColors.added;
|
||||
case 'selected':
|
||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||
case 'accelerated':
|
||||
return auditColors.accelerated;
|
||||
case 'found':
|
||||
if (tx.context === 'projected') {
|
||||
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
|
||||
} else {
|
||||
return feeLevelColor;
|
||||
}
|
||||
default:
|
||||
if (tx.acc) {
|
||||
return auditColors.accelerated;
|
||||
} else {
|
||||
return feeLevelColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@
|
||||
<app-fee-rate [fee]="feeRate"></app-fee-rate>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="effectiveRate && effectiveRate !== feeRate">
|
||||
<tr *ngIf="hasEffectiveRate && effectiveRate != null">
|
||||
<td *ngIf="!this.acceleration" class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
||||
<td *ngIf="this.acceleration" class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Accelerated fee rate</td>
|
||||
<td>
|
||||
@@ -55,9 +55,13 @@
|
||||
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
|
||||
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
|
||||
<td *ngSwitchCase="'rbf'"><span class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span></td>
|
||||
<td *ngSwitchCase="'accelerated'"><span class="badge badge-success" i18n="transaction.audit.accelerated">Accelerated</span></td>
|
||||
<td *ngSwitchCase="'accelerated'"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></td>
|
||||
</ng-container>
|
||||
</tr>
|
||||
<tr *ngIf="!auditEnabled && tx && tx.status === 'accelerated'">
|
||||
<td class="td-width"></td>
|
||||
<td><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
text-align: left;
|
||||
min-width: 320px;
|
||||
pointer-events: none;
|
||||
z-index: 11;
|
||||
|
||||
&.clickable {
|
||||
pointer-events: all;
|
||||
@@ -19,4 +20,17 @@
|
||||
|
||||
.td-width {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.badge.badge-accelerated {
|
||||
background-color: #653b9c;
|
||||
box-shadow: #ad7de57f 0px 0px 12px -2px;
|
||||
color: white;
|
||||
animation: acceleratePulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes acceleratePulse {
|
||||
0% { background-color: #653b9c; box-shadow: #ad7de57f 0px 0px 12px -2px; }
|
||||
50% { background-color: #8457bb; box-shadow: #ad7de5 0px 0px 18px -2px;}
|
||||
100% { background-color: #653b9c; box-shadow: #ad7de57f 0px 0px 12px -2px; }
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Component, ElementRef, ViewChild, Input, OnChanges, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { TransactionStripped } from '../../interfaces/websocket.interface';
|
||||
import { Position } from '../../components/block-overview-graph/sprite-types.js';
|
||||
import { Price } from '../../services/price.service';
|
||||
import { TransactionStripped } from '../../interfaces/node-api.interface.js';
|
||||
import { TransactionFlags } from '../../shared/filters.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block-overview-tooltip',
|
||||
@@ -22,6 +23,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
|
||||
feeRate = 0;
|
||||
effectiveRate;
|
||||
acceleration;
|
||||
hasEffectiveRate: boolean = false;
|
||||
|
||||
tooltipPosition: Position = { x: 0, y: 0 };
|
||||
|
||||
@@ -55,6 +57,8 @@ export class BlockOverviewTooltipComponent implements OnChanges {
|
||||
this.feeRate = this.fee / this.vsize;
|
||||
this.effectiveRate = tx.rate;
|
||||
this.acceleration = tx.acc;
|
||||
this.hasEffectiveRate = Math.abs((this.fee / this.vsize) - this.effectiveRate) > 0.05
|
||||
|| (tx.bigintFlags && (tx.bigintFlags & (TransactionFlags.cpfp_child | TransactionFlags.cpfp_parent)) > 0n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { echarts, EChartsOption } from '../../graphs/echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
@@ -123,11 +123,11 @@ export class BlockRewardsGraphComponent implements OnInit {
|
||||
title: title,
|
||||
animation: false,
|
||||
color: [
|
||||
new graphic.LinearGradient(0, 0, 0, 1, [
|
||||
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#FDD835' },
|
||||
{ offset: 1, color: '#FB8C00' },
|
||||
]),
|
||||
new graphic.LinearGradient(0, 0, 0, 1, [
|
||||
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#C0CA33' },
|
||||
{ offset: 1, color: '#1B5E20' },
|
||||
]),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
||||
import { EChartsOption} from 'echarts';
|
||||
import { EChartsOption} from '../../graphs/echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="block-wrapper">
|
||||
<div class="block-container">
|
||||
<app-block-overview-graph
|
||||
#blockGraph
|
||||
[isLoading]="false"
|
||||
[resolution]="resolution"
|
||||
[blockLimit]="stateService.blockVSize"
|
||||
[orientation]="'top'"
|
||||
[flip]="false"
|
||||
[disableSpinner]="true"
|
||||
(txClickEvent)="onTxClick($event)"
|
||||
></app-block-overview-graph>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
.block-wrapper {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: #181b2d;
|
||||
}
|
||||
|
||||
.block-container {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
width: 100vw;
|
||||
max-width: 100vh;
|
||||
height: 100vh;
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
* {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
180
frontend/src/app/components/block-view/block-view.component.ts
Normal file
180
frontend/src/app/components/block-view/block-view.component.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { Component, OnInit, OnDestroy, ViewChild, HostListener } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { switchMap, tap, catchError, shareReplay, filter } from 'rxjs/operators';
|
||||
import { of, Subscription } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
|
||||
function bestFitResolution(min, max, n): number {
|
||||
const target = (min + max) / 2;
|
||||
let bestScore = Infinity;
|
||||
let best = null;
|
||||
for (let i = min; i <= max; i++) {
|
||||
const remainder = (n % i);
|
||||
if (remainder < bestScore || (remainder === bestScore && (Math.abs(i - target) < Math.abs(best - target)))) {
|
||||
bestScore = remainder;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-block-view',
|
||||
templateUrl: './block-view.component.html',
|
||||
styleUrls: ['./block-view.component.scss']
|
||||
})
|
||||
export class BlockViewComponent implements OnInit, OnDestroy {
|
||||
network = '';
|
||||
block: BlockExtended;
|
||||
blockHeight: number;
|
||||
blockHash: string;
|
||||
rawId: string;
|
||||
isLoadingBlock = true;
|
||||
strippedTransactions: TransactionStripped[];
|
||||
isLoadingOverview = true;
|
||||
autofit: boolean = false;
|
||||
resolution: number = 80;
|
||||
|
||||
overviewSubscription: Subscription;
|
||||
networkChangedSubscription: Subscription;
|
||||
queryParamsSubscription: Subscription;
|
||||
|
||||
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
public stateService: StateService,
|
||||
private seoService: SeoService,
|
||||
private apiService: ApiService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.network = this.stateService.network;
|
||||
|
||||
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
|
||||
this.autofit = params.autofit === 'true';
|
||||
if (this.autofit) {
|
||||
this.onResize();
|
||||
}
|
||||
});
|
||||
|
||||
const block$ = this.route.paramMap.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.rawId = params.get('id') || '';
|
||||
|
||||
const blockHash: string = params.get('id') || '';
|
||||
this.block = undefined;
|
||||
|
||||
let isBlockHeight = false;
|
||||
if (/^[0-9]+$/.test(blockHash)) {
|
||||
isBlockHeight = true;
|
||||
} else {
|
||||
this.blockHash = blockHash;
|
||||
}
|
||||
|
||||
this.isLoadingBlock = true;
|
||||
this.isLoadingOverview = true;
|
||||
|
||||
if (isBlockHeight) {
|
||||
return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockHash, 10))
|
||||
.pipe(
|
||||
switchMap((hash) => {
|
||||
if (hash) {
|
||||
this.blockHash = hash;
|
||||
return this.apiService.getBlock$(hash);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
catchError(() => {
|
||||
return of(null);
|
||||
}),
|
||||
);
|
||||
}
|
||||
return this.apiService.getBlock$(blockHash);
|
||||
}),
|
||||
filter((block: BlockExtended | void) => block != null),
|
||||
tap((block: BlockExtended) => {
|
||||
this.block = block;
|
||||
this.blockHeight = block.height;
|
||||
|
||||
this.seoService.setTitle($localize`:@@block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.id}:BLOCK_ID:`);
|
||||
if( this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet' ) {
|
||||
this.seoService.setDescription($localize`:@@meta.description.liquid.block:See size, weight, fee range, included transactions, and more for Liquid${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`);
|
||||
} else {
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.block:See size, weight, fee range, included transactions, audit (expected v actual), and more for Bitcoin${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`);
|
||||
}
|
||||
this.isLoadingBlock = false;
|
||||
this.isLoadingOverview = true;
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
this.overviewSubscription = block$.pipe(
|
||||
switchMap((block) => this.apiService.getStrippedBlockTransactions$(block.id)
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
return of([]);
|
||||
}),
|
||||
switchMap((transactions) => {
|
||||
return of(transactions);
|
||||
})
|
||||
)
|
||||
),
|
||||
)
|
||||
.subscribe((transactions: TransactionStripped[]) => {
|
||||
this.strippedTransactions = transactions;
|
||||
this.isLoadingOverview = false;
|
||||
if (this.blockGraph) {
|
||||
this.blockGraph.destroy();
|
||||
this.blockGraph.setup(this.strippedTransactions);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this.isLoadingOverview = false;
|
||||
if (this.blockGraph) {
|
||||
this.blockGraph.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
this.networkChangedSubscription = this.stateService.networkChanged$
|
||||
.subscribe((network) => this.network = network);
|
||||
}
|
||||
|
||||
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
|
||||
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
|
||||
if (!event.keyModifier) {
|
||||
this.router.navigate([url]);
|
||||
} else {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(): void {
|
||||
if (this.autofit) {
|
||||
this.resolution = bestFitResolution(64, 96, Math.min(window.innerWidth, window.innerHeight));
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.overviewSubscription) {
|
||||
this.overviewSubscription.unsubscribe();
|
||||
}
|
||||
if (this.networkChangedSubscription) {
|
||||
this.networkChangedSubscription.unsubscribe();
|
||||
}
|
||||
if (this.queryParamsSubscription) {
|
||||
this.queryParamsSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/co
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { switchMap, tap, throttleTime, catchError, shareReplay, startWith, pairwise, filter } from 'rxjs/operators';
|
||||
import { of, Subscription, asyncScheduler } from 'rxjs';
|
||||
import { of, Subscription, asyncScheduler, forkJoin } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { OpenGraphService } from '../../services/opengraph.service';
|
||||
@@ -10,6 +10,7 @@ import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.in
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
|
||||
import { ServicesApiServices } from '../../services/services-api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block-preview',
|
||||
@@ -42,7 +43,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
||||
public stateService: StateService,
|
||||
private seoService: SeoService,
|
||||
private openGraphService: OpenGraphService,
|
||||
private apiService: ApiService
|
||||
private apiService: ApiService,
|
||||
private servicesApiService: ServicesApiServices,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
@@ -121,21 +123,37 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
||||
this.overviewSubscription = block$.pipe(
|
||||
startWith(null),
|
||||
pairwise(),
|
||||
switchMap(([prevBlock, block]) => this.apiService.getStrippedBlockTransactions$(block.id)
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
this.overviewError = err;
|
||||
this.openGraphService.fail('block-viz-' + this.rawId);
|
||||
return of([]);
|
||||
}),
|
||||
switchMap((transactions) => {
|
||||
return of({ transactions, direction: 'down' });
|
||||
})
|
||||
)
|
||||
switchMap(([prevBlock, block]) => {
|
||||
return forkJoin([
|
||||
this.apiService.getStrippedBlockTransactions$(block.id)
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
this.overviewError = err;
|
||||
this.openGraphService.fail('block-viz-' + this.rawId);
|
||||
return of([]);
|
||||
}),
|
||||
switchMap((transactions) => {
|
||||
return of(transactions);
|
||||
})
|
||||
),
|
||||
this.stateService.env.ACCELERATOR === true && block.height > 819500 ? this.servicesApiService.getAccelerationHistory$({ blockHash: block.id }) : of([])
|
||||
]);
|
||||
}
|
||||
),
|
||||
)
|
||||
.subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => {
|
||||
.subscribe(([transactions, accelerations]) => {
|
||||
this.strippedTransactions = transactions;
|
||||
|
||||
const acceleratedInBlock = {};
|
||||
for (const acc of accelerations) {
|
||||
acceleratedInBlock[acc.txid] = acc;
|
||||
}
|
||||
for (const tx of transactions) {
|
||||
if (acceleratedInBlock[tx.txid]) {
|
||||
tx.acc = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.isLoadingOverview = false;
|
||||
if (this.blockGraph) {
|
||||
this.blockGraph.destroy();
|
||||
|
||||
@@ -42,12 +42,12 @@
|
||||
<ng-container *ngIf="!isLoadingBlock; else skeletonRows">
|
||||
<tr>
|
||||
<td class="td-width" i18n="block.hash">Hash</td>
|
||||
<td>‎<a [routerLink]="['/block/' | relativeUrl, block.id]" title="{{ block.id }}">{{ block.id | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.id"></app-clipboard></td>
|
||||
<td>‎<a [routerLink]="['/block/' | relativeUrl, block.id]" title="{{ block.id }}">{{ block.id | shortenString : 13 }}</a> <app-clipboard [text]="block.id"></app-clipboard></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.timestamp">Timestamp</td>
|
||||
<td>
|
||||
<app-timestamp [unixTime]="block.timestamp" [precision]="1" minUnit="minute"></app-timestamp>
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="block.timestamp" [precision]="1" minUnit="minute"></app-timestamp>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -59,7 +59,7 @@
|
||||
<td [innerHTML]="'‎' + (block.weight | wuBytes: 2)"></td>
|
||||
</tr>
|
||||
<tr *ngIf="auditAvailable">
|
||||
<td><ng-container i18n="latest-blocks.health">Health</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="what-is-block-health"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></td>
|
||||
<td><ng-container i18n="latest-blocks.health">Health</ng-container><a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="what-is-block-health"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></td>
|
||||
<td>
|
||||
<span
|
||||
class="health-badge badge"
|
||||
@@ -115,6 +115,8 @@
|
||||
[orientation]="'top'"
|
||||
[flip]="false"
|
||||
[blockConversion]="blockConversion"
|
||||
[showFilters]="true"
|
||||
[excludeFilters]="['replacement']"
|
||||
(txClickEvent)="onTxClick($event)"
|
||||
></app-block-overview-graph>
|
||||
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
|
||||
@@ -219,33 +221,39 @@
|
||||
<div class="box" *ngIf="!error && webGlEnabled && showAudit">
|
||||
<div class="nav nav-tabs" *ngIf="isMobile && showAudit">
|
||||
<a class="nav-link" [class.active]="mode === 'projected'"
|
||||
fragment="projected" (click)="changeMode('projected')"><ng-container i18n="block.expected">Expected</ng-container> <span class="badge badge-pill badge-warning" i18n="beta">beta</span></a>
|
||||
fragment="projected" (click)="changeMode('projected')"><ng-container i18n="block.expected">Expected</ng-container></a>
|
||||
<a class="nav-link" [class.active]="mode === 'actual'" i18n="block.actual"
|
||||
fragment="actual" (click)="changeMode('actual')">Actual</a>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<h3 class="block-subtitle" *ngIf="!isMobile"><ng-container i18n="block.expected-block">Expected Block</ng-container> <span class="badge badge-pill badge-warning beta" i18n="beta">beta</span></h3>
|
||||
<h3 class="block-subtitle" *ngIf="!isMobile"><ng-container i18n="block.expected-block">Expected Block</ng-container></h3>
|
||||
<div class="block-graph-wrapper">
|
||||
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="86"
|
||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit"
|
||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"></app-block-overview-graph>
|
||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"
|
||||
[showFilters]="true" [excludeFilters]="['replacement']"></app-block-overview-graph>
|
||||
<ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container>
|
||||
</div>
|
||||
<ng-container *ngIf="network !== 'liquid'">
|
||||
<ng-container *ngTemplateOutlet="isMobile && mode === 'actual' ? actualDetails : expectedDetails"></ng-container>
|
||||
<ng-template [ngIf]="!isLoadingOverview" [ngIfElse]="loadingDetailsSkeletons">
|
||||
<ng-container *ngTemplateOutlet="isMobile && mode === 'actual' ? actualDetails : expectedDetails"></ng-container>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="col-sm" *ngIf="!isMobile">
|
||||
<h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3>
|
||||
<h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container><a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3>
|
||||
<div class="block-graph-wrapper">
|
||||
<app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="86"
|
||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined" [auditHighlighting]="showAudit"
|
||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"></app-block-overview-graph>
|
||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"
|
||||
[showFilters]="true" [excludeFilters]="['replacement']"></app-block-overview-graph>
|
||||
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
|
||||
</div>
|
||||
<ng-container *ngIf="network !== 'liquid'">
|
||||
<ng-container *ngTemplateOutlet="actualDetails"></ng-container>
|
||||
<ng-template [ngIf]="!isLoadingOverview" [ngIfElse]="loadingDetailsSkeletons">
|
||||
<ng-container *ngTemplateOutlet="actualDetails"></ng-container>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
@@ -262,7 +270,7 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width" i18n="transaction.version">Version</td>
|
||||
<td>{{ block.version | decimal2hex }} <span *ngIf="displayTaprootStatus() && hasTaproot(block.version)" class="badge badge-success ml-1" >Taproot</span></td>
|
||||
<td>{{ block.version | decimal2hex }} <span *ngIf="displayTaprootStatus() && hasTaproot(block.version)" class="badge badge-success ml-1" i18n="tx-features.tag.taproot|Taproot">Taproot</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||
<td i18n="block.bits">Bits</td>
|
||||
@@ -452,5 +460,24 @@
|
||||
</table>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #loadingDetailsSkeletons>
|
||||
<table class="table table-borderless table-striped audit-details-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="w-50" i18n="block.total-fees|Total fees in a block">Total fees</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.weight">Weight</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="mempool-block.transactions">Transactions</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-template>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
@@ -57,11 +57,6 @@
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
.info-link {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.difference {
|
||||
margin-left: 0.5em;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { detectWebGL } from '../../shared/graphs.utils';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { PriceService, Price } from '../../services/price.service';
|
||||
import { CacheService } from '../../services/cache.service';
|
||||
import { ServicesApiServices } from '../../services/services-api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block',
|
||||
@@ -103,6 +104,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
private apiService: ApiService,
|
||||
private priceService: PriceService,
|
||||
private cacheService: CacheService,
|
||||
private servicesApiService: ServicesApiServices,
|
||||
) {
|
||||
this.webGlEnabled = detectWebGL();
|
||||
}
|
||||
@@ -166,7 +168,6 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.page = 1;
|
||||
this.error = undefined;
|
||||
this.fees = undefined;
|
||||
this.stateService.markBlock$.next({});
|
||||
|
||||
if (history.state.data && history.state.data.blockHeight) {
|
||||
this.blockHeight = history.state.data.blockHeight;
|
||||
@@ -176,6 +177,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
let isBlockHeight = false;
|
||||
if (/^[0-9]+$/.test(blockHash)) {
|
||||
isBlockHeight = true;
|
||||
this.stateService.markBlock$.next({ blockHeight: parseInt(blockHash, 10)});
|
||||
} else {
|
||||
this.blockHash = blockHash;
|
||||
}
|
||||
@@ -202,6 +204,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.location.replaceState(
|
||||
this.router.createUrlTree([(this.network ? '/' + this.network : '') + '/block/', hash]).toString()
|
||||
);
|
||||
this.seoService.updateCanonical(this.location.path());
|
||||
return this.apiService.getBlock$(hash).pipe(
|
||||
catchError((err) => {
|
||||
this.error = err;
|
||||
@@ -327,17 +330,28 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.overviewError = err;
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
),
|
||||
this.stateService.env.ACCELERATOR === true && block.height > 819500 ? this.servicesApiService.getAccelerationHistory$({ blockHash: block.id }) : of([])
|
||||
]);
|
||||
})
|
||||
)
|
||||
.subscribe(([transactions, blockAudit]) => {
|
||||
.subscribe(([transactions, blockAudit, accelerations]) => {
|
||||
if (transactions) {
|
||||
this.strippedTransactions = transactions;
|
||||
} else {
|
||||
this.strippedTransactions = [];
|
||||
}
|
||||
|
||||
const acceleratedInBlock = {};
|
||||
for (const acc of accelerations) {
|
||||
acceleratedInBlock[acc.txid] = acc;
|
||||
}
|
||||
for (const tx of transactions) {
|
||||
if (acceleratedInBlock[tx.txid]) {
|
||||
tx.acc = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.blockAudit = null;
|
||||
if (transactions && blockAudit) {
|
||||
const inTemplate = {};
|
||||
|
||||
43
frontend/src/app/components/block/block.module.ts
Normal file
43
frontend/src/app/components/block/block.module.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { BlockComponent } from './block.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: ':id',
|
||||
component: BlockComponent,
|
||||
data: {
|
||||
ogImage: true
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes)
|
||||
],
|
||||
exports: [
|
||||
RouterModule
|
||||
]
|
||||
})
|
||||
export class BlockRoutingModule { }
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
BlockRoutingModule,
|
||||
SharedModule,
|
||||
],
|
||||
declarations: [
|
||||
BlockComponent,
|
||||
]
|
||||
})
|
||||
export class BlockModule { }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
<ng-template #emptyfees>
|
||||
<div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">
|
||||
|
||||
<app-fee-rate unitClass=""></app-fee-rate>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-fee-span'" class="fee-span"
|
||||
@@ -37,7 +37,7 @@
|
||||
</div>
|
||||
<ng-template #emptyfeespan>
|
||||
<div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fee-span">
|
||||
|
||||
<app-fee-rate unitClass=""></app-fee-rate>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-total-fees'" *ngIf="showMiningInfo"
|
||||
@@ -56,7 +56,7 @@
|
||||
<app-time kind="since" [time]="block.timestamp" [fastRender]="true" [precision]="1" minUnit="minute"></app-time></div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined">
|
||||
<div class="animated" [class]="markHeight === block.height ? 'hide' : 'show'" *ngIf="block.extras?.pool != undefined">
|
||||
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge badge-primary"
|
||||
[routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
|
||||
{{ block.extras.pool.name}}</a>
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
opacity: 1;
|
||||
}
|
||||
.hide {
|
||||
opacity: 0;
|
||||
opacity: 0.4;
|
||||
pointer-events : none;
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
enabledMiningInfoIfNeeded(url) {
|
||||
this.showMiningInfo = url.indexOf('/mining') !== -1;
|
||||
this.showMiningInfo = url.includes('/mining') || url.includes('/acceleration');
|
||||
this.cd.markForCheck(); // Need to update the view asap
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="text-center" class="blockchain-wrapper" [class.time-ltr]="timeLtr" [class.ltr-transition]="ltrTransitionEnabled" #container>
|
||||
<div class="position-container" [ngClass]="network ? network : ''" [style.--divider-offset]="dividerOffset + 'px'" [style.--mempool-offset]="mempoolOffset + 'px'">
|
||||
<div #positionContainer class="position-container" [ngClass]="network ? network : ''" [style]="positionStyle">
|
||||
<span>
|
||||
<div class="blocks-wrapper">
|
||||
<div class="scroll-spacer" *ngIf="minScrollWidth" [style.left]="minScrollWidth + 'px'"></div>
|
||||
|
||||
@@ -26,15 +26,7 @@
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 75px;
|
||||
--divider-offset: 50vw;
|
||||
--mempool-offset: 0px;
|
||||
transform: translateX(calc(var(--divider-offset) + var(--mempool-offset)));
|
||||
}
|
||||
|
||||
.blockchain-wrapper.time-ltr {
|
||||
.position-container {
|
||||
transform: translateX(calc(100vw - var(--divider-offset) - var(--mempool-offset)));
|
||||
}
|
||||
transform: translateX(1280px);
|
||||
}
|
||||
|
||||
.black-background {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, Output, EventEmitter, HostListener, ChangeDetectorRef, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, Output, EventEmitter, ChangeDetectorRef, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { firstValueFrom, Subscription } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@@ -27,8 +27,11 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
|
||||
loadingTip: boolean = true;
|
||||
connected: boolean = true;
|
||||
|
||||
dividerOffset: number = 0;
|
||||
mempoolOffset: number = 0;
|
||||
dividerOffset: number | null = null;
|
||||
mempoolOffset: number | null = null;
|
||||
positionStyle = {
|
||||
transform: "translateX(1280px)",
|
||||
};
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
@@ -40,6 +43,7 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
|
||||
this.network = this.stateService.network;
|
||||
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
|
||||
this.timeLtr = !!ltr;
|
||||
this.updateStyle();
|
||||
});
|
||||
this.connectionStateSubscription = this.stateService.connectionState$.subscribe(state => {
|
||||
this.connected = (state === 2);
|
||||
@@ -63,29 +67,47 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
|
||||
const prevOffset = this.mempoolOffset;
|
||||
this.mempoolOffset = 0;
|
||||
this.mempoolOffsetChange.emit(0);
|
||||
this.updateStyle();
|
||||
setTimeout(() => {
|
||||
this.ltrTransitionEnabled = true;
|
||||
this.flipping = true;
|
||||
this.stateService.timeLtr.next(!this.timeLtr);
|
||||
this.cd.markForCheck();
|
||||
setTimeout(() => {
|
||||
this.ltrTransitionEnabled = false;
|
||||
this.flipping = false;
|
||||
this.mempoolOffset = prevOffset;
|
||||
this.mempoolOffsetChange.emit(this.mempoolOffset);
|
||||
this.mempoolOffsetChange.emit((this.mempoolOffset || 0));
|
||||
this.updateStyle();
|
||||
this.cd.markForCheck();
|
||||
}, 1000);
|
||||
}, 0);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onMempoolWidthChange(width): void {
|
||||
if (this.flipping) {
|
||||
return;
|
||||
}
|
||||
this.mempoolOffset = Math.max(0, width - this.dividerOffset);
|
||||
this.cd.markForCheck();
|
||||
this.mempoolOffset = Math.max(0, width - (this.dividerOffset || 0));
|
||||
this.updateStyle();
|
||||
this.mempoolOffsetChange.emit(this.mempoolOffset);
|
||||
}
|
||||
|
||||
updateStyle(): void {
|
||||
if (this.dividerOffset == null || this.mempoolOffset == null) {
|
||||
return;
|
||||
}
|
||||
const oldTransform = this.positionStyle.transform;
|
||||
this.positionStyle = this.timeLtr ? {
|
||||
transform: `translateX(calc(100vw - ${this.dividerOffset + this.mempoolOffset}px)`,
|
||||
} : {
|
||||
transform: `translateX(${this.dividerOffset + this.mempoolOffset}px)`,
|
||||
};
|
||||
if (oldTransform !== this.positionStyle.transform) {
|
||||
this.cd.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.containerWidth) {
|
||||
this.onResize();
|
||||
@@ -107,6 +129,6 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
|
||||
this.dividerOffset = width * 0.95;
|
||||
}
|
||||
}
|
||||
this.cd.markForCheck();
|
||||
this.updateStyle();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
|
||||
|
||||
<div class="container-xl" style="min-height: 335px" [ngClass]="{'widget': widget, 'full-height': !widget, 'legacy': !indexingAvailable}">
|
||||
<div class="container-xl" style="min-height: 335px" [ngClass]="{'widget': widget, 'full-height': !widget, 'legacy': !isMempoolModule}">
|
||||
<h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Blocks</h1>
|
||||
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
|
||||
|
||||
@@ -9,28 +9,28 @@
|
||||
<div style="min-height: 295px">
|
||||
<table class="table table-borderless">
|
||||
<thead>
|
||||
<th class="height text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}" i18n="latest-blocks.height">Height</th>
|
||||
<th *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}" i18n="mining.pool-name"
|
||||
<th class="height text-left" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}" i18n="latest-blocks.height">Height</th>
|
||||
<th *ngIf="isMempoolModule" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}" i18n="mining.pool-name"
|
||||
i18n-ngbTooltip="mining.pool-name" ngbTooltip="Pool" placement="bottom" #miningpool [disableTooltip]="!isEllipsisActive(miningpool)">Pool</th>
|
||||
<th class="timestamp" i18n="latest-blocks.timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Timestamp</th>
|
||||
<th *ngIf="auditAvailable" class="health text-right" i18n="latest-blocks.health" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"
|
||||
<th class="timestamp" i18n="latest-blocks.timestamp" *ngIf="!widget" [class]="isMempoolModule ? '' : 'legacy'">Timestamp</th>
|
||||
<th *ngIf="auditAvailable" class="health text-right" i18n="latest-blocks.health" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}"
|
||||
i18n-ngbTooltip="latest-blocks.health" ngbTooltip="Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Health</th>
|
||||
<th *ngIf="indexingAvailable" class="reward text-right" i18n="latest-blocks.reward" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"
|
||||
<th *ngIf="isMempoolModule" class="reward text-right" i18n="latest-blocks.reward" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}"
|
||||
i18n-ngbTooltip="latest-blocks.reward" ngbTooltip="Reward" placement="bottom" #reward [disableTooltip]="!isEllipsisActive(reward)">Reward</th>
|
||||
<th *ngIf="indexingAvailable && !widget" class="fees text-right" i18n="latest-blocks.fees" [class]="indexingAvailable ? '' : 'legacy'">Fees</th>
|
||||
<th *ngIf="auditAvailable && !widget" class="fee-delta" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"></th>
|
||||
<th *ngIf="indexingAvailable" class="txs text-right" i18n="dashboard.txs" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"
|
||||
<th *ngIf="isMempoolModule && !auditAvailable || isMempoolModule && !widget" class="fees text-right" i18n="latest-blocks.fees" [class]="isMempoolModule ? '' : 'legacy'">Fees</th>
|
||||
<th *ngIf="auditAvailable && !widget" class="fee-delta" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}"></th>
|
||||
<th *ngIf="isMempoolModule" class="txs text-right" i18n="dashboard.txs" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}"
|
||||
i18n-ngbTooltip="dashboard.txs" ngbTooltip="TXs" placement="bottom" #txs [disableTooltip]="!isEllipsisActive(txs)">TXs</th>
|
||||
<th *ngIf="!indexingAvailable" class="txs text-right" i18n="dashboard.txs" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">Transactions</th>
|
||||
<th class="size" i18n="latest-blocks.size" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Size</th>
|
||||
<th *ngIf="!isMempoolModule" class="txs text-right" i18n="dashboard.txs" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">Transactions</th>
|
||||
<th class="size" i18n="latest-blocks.size" *ngIf="!widget" [class]="isMempoolModule ? '' : 'legacy'">Size</th>
|
||||
</thead>
|
||||
<tbody *ngIf="blocks$ | async as blocks; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
<tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
|
||||
<td class="height text-left" [class]="widget ? 'widget' : ''">
|
||||
<a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<div class="tooltip-custom">
|
||||
<td *ngIf="isMempoolModule" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<div *ngIf="indexingAvailable" class="tooltip-custom">
|
||||
<a class="clear-link" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]">
|
||||
<img width="22" height="22" src="{{ block.extras.pool['logo'] }}"
|
||||
onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
|
||||
@@ -38,11 +38,17 @@
|
||||
</a>
|
||||
<span *ngIf="!widget" class="tooltiptext badge badge-secondary scriptmessage">{{ block.extras.coinbaseRaw | hex2ascii }}</span>
|
||||
</div>
|
||||
<div *ngIf="!indexingAvailable" class="tooltip-custom">
|
||||
<img width="22" height="22" src="{{ block.extras.pool['logo'] }}"
|
||||
onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
|
||||
<span class="pool-name">{{ block.extras.pool.name }}</span>
|
||||
<span *ngIf="!widget" class="tooltiptext badge badge-secondary scriptmessage">{{ block.extras.coinbaseRaw | hex2ascii }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="timestamp" *ngIf="!widget" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||
<td class="timestamp" *ngIf="!widget" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
|
||||
</td>
|
||||
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<a
|
||||
*ngIf="block?.extras?.matchRate != null; else nullHealth"
|
||||
class="health-badge badge"
|
||||
@@ -56,21 +62,21 @@
|
||||
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<td *ngIf="isMempoolModule" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<app-amount [satoshis]="block.extras.reward" [noFiat]="true" digitsInfo="1.2-2"></app-amount>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable && !widget" class="fees text-right" [class]="indexingAvailable ? '' : 'legacy'">
|
||||
<td *ngIf="isMempoolModule && !auditAvailable || isMempoolModule && !widget" class="fees text-right" [class]="isMempoolModule ? '' : 'legacy'">
|
||||
<app-amount [satoshis]="block.extras.totalFees" [noFiat]="true" digitsInfo="1.2-2"></app-amount>
|
||||
</td>
|
||||
<td *ngIf="auditAvailable" class="fee-delta" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<td *ngIf="auditAvailable" class="fee-delta" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<span *ngIf="block.extras.feeDelta" class="difference" [class.positive]="block.extras.feeDelta >= 0" [class.negative]="block.extras.feeDelta < 0">
|
||||
{{ block.extras.feeDelta > 0 ? '+' : '' }}{{ (block.extras.feeDelta * 100) | amountShortener: 2 }}%
|
||||
</span>
|
||||
</td>
|
||||
<td class="txs text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<td class="txs text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
{{ block.tx_count | number }}
|
||||
</td>
|
||||
<td class="size" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
||||
<td class="size" *ngIf="!widget" [class]="isMempoolModule ? '' : 'legacy'">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-mempool" role="progressbar"
|
||||
[ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"></div>
|
||||
@@ -82,34 +88,31 @@
|
||||
<ng-template #skeleton>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of skeletonLines">
|
||||
<td class="height text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<td class="height text-left" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<span class="skeleton-loader" style="max-width: 125px"></span>
|
||||
</td>
|
||||
<td class="timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
||||
<td *ngIf="isMempoolModule" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<span class="skeleton-loader" style="max-width: 150px"></span>
|
||||
</td>
|
||||
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
||||
<span class="skeleton-loader" style="max-width: 125px"></span>
|
||||
<td class="timestamp" *ngIf="!widget" [class]="isMempoolModule ? '' : 'legacy'">
|
||||
<span class="skeleton-loader" style="max-width: 150px"></span>
|
||||
</td>
|
||||
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<td *ngIf="isMempoolModule" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable && !widget" class="fees text-right" [class]="indexingAvailable ? '' : 'legacy'">
|
||||
<td *ngIf="isMempoolModule && !auditAvailable || isMempoolModule && !widget" class="fees text-right" [class]="isMempoolModule ? '' : 'legacy'">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td *ngIf="auditAvailable && !widget" class="fee-delta" [class]="indexingAvailable ? '' : 'legacy'">
|
||||
<td *ngIf="auditAvailable && !widget" class="fee-delta" [class]="isMempoolModule ? '' : 'legacy'">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td class="txs text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<td class="txs text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td class="size" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
||||
<td class="size" *ngIf="!widget" [class]="isMempoolModule ? '' : 'legacy'">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -19,6 +19,7 @@ export class BlocksList implements OnInit {
|
||||
|
||||
blocks$: Observable<BlockExtended[]> = undefined;
|
||||
|
||||
isMempoolModule = false;
|
||||
indexingAvailable = false;
|
||||
auditAvailable = false;
|
||||
isLoading = true;
|
||||
@@ -39,6 +40,7 @@ export class BlocksList implements OnInit {
|
||||
private cd: ChangeDetectorRef,
|
||||
private seoService: SeoService,
|
||||
) {
|
||||
this.isMempoolModule = this.stateService.env.BASE_MODULE === 'mempool';
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -53,7 +55,9 @@ export class BlocksList implements OnInit {
|
||||
this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
|
||||
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
|
||||
|
||||
this.seoService.setTitle($localize`:@@meta.title.blocks-list:Blocks`);
|
||||
if (!this.widget) {
|
||||
this.seoService.setTitle($localize`:@@m8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`);
|
||||
}
|
||||
if( this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet' ) {
|
||||
this.seoService.setDescription($localize`:@@meta.description.liquid.blocks:See the most recent Liquid${seoDescriptionNetwork(this.stateService.network)} blocks along with basic stats such as block height, block size, and more.`);
|
||||
} else {
|
||||
@@ -75,11 +79,10 @@ export class BlocksList implements OnInit {
|
||||
this.lastBlockHeight = Math.max(...blocks.map(o => o.height));
|
||||
}),
|
||||
map(blocks => {
|
||||
if (this.indexingAvailable) {
|
||||
if (this.stateService.env.BASE_MODULE === 'mempool') {
|
||||
for (const block of blocks) {
|
||||
// @ts-ignore: Need to add an extra field for the template
|
||||
block.extras.pool.logo = `/resources/mining-pools/` +
|
||||
block.extras.pool.slug + '.svg';
|
||||
block.extras.pool.logo = `/resources/mining-pools/` + block.extras.pool.slug + '.svg';
|
||||
}
|
||||
}
|
||||
if (this.widget) {
|
||||
@@ -110,7 +113,7 @@ export class BlocksList implements OnInit {
|
||||
}
|
||||
if (blocks[1]) {
|
||||
this.blocksCount = Math.max(this.blocksCount, blocks[1][0].height) + 1;
|
||||
if (this.stateService.env.MINING_DASHBOARD) {
|
||||
if (this.isMempoolModule) {
|
||||
// @ts-ignore: Need to add an extra field for the template
|
||||
blocks[1][0].extras.pool.logo = `/resources/mining-pools/` +
|
||||
blocks[1][0].extras.pool.slug + '.svg';
|
||||
@@ -121,9 +124,11 @@ export class BlocksList implements OnInit {
|
||||
return acc;
|
||||
}, []),
|
||||
switchMap((blocks) => {
|
||||
blocks.forEach(block => {
|
||||
block.extras.feeDelta = block.extras.expectedFees ? (block.extras.totalFees - block.extras.expectedFees) / block.extras.expectedFees : 0;
|
||||
});
|
||||
if (this.isMempoolModule && this.auditAvailable) {
|
||||
blocks.forEach(block => {
|
||||
block.extras.feeDelta = block.extras.expectedFees ? (block.extras.totalFees - block.extras.expectedFees) / block.extras.expectedFees : 0;
|
||||
});
|
||||
}
|
||||
return of(blocks);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="container-xl">
|
||||
<div class="text-center">
|
||||
<h2>Calculator</h2>
|
||||
<h2 i18n="shared.calculator">Calculator</h2>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="price$ | async; else loading">
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
<div class="input-group input-group-lg mb-1">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">sats</span>
|
||||
<span class="input-group-text" i18n="shared.sats">sats</span>
|
||||
</div>
|
||||
<input type="text" class="form-control" formControlName="satoshis" (input)="transformInput('satoshis')" (click)="selectAll($event)">
|
||||
<app-clipboard [button]="true" [text]="form.get('satoshis').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
|
||||
@@ -41,7 +41,7 @@
|
||||
<div class="bitcoin-satoshis-text">
|
||||
₿
|
||||
<span [innerHTML]="form.get('bitcoin').value | bitcoinsatoshis"></span>
|
||||
<span class="sats"> sats</span>
|
||||
<span class="sats" i18n="shared.sats">sats</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -38,36 +38,35 @@
|
||||
</div>
|
||||
<ng-container *ngIf="!hideStats">
|
||||
<div class="stats top left">
|
||||
<p class="label" i18n="clock.fiat-price">fiat price</p>
|
||||
<p class="label" i18n>Price</p>
|
||||
<p>
|
||||
<app-fiat [value]="100000000" digitsInfo="1.2-2" colorClass="white-color"></app-fiat>
|
||||
</p>
|
||||
</div>
|
||||
<div class="stats top right">
|
||||
<p class="label" i18n="clock.priority-rate|priority fee rate">priority rate</p>
|
||||
<p class="label" i18n="fees-box.high-priority">High Priority</p>
|
||||
<p *ngIf="recommendedFees$ | async as recommendedFees;">
|
||||
<app-fee-rate [fee]="recommendedFees.fastestFee" unitClass="" rounding="1.0-0"></app-fee-rate>
|
||||
</p>
|
||||
</div>
|
||||
<div *ngIf="mode !== 'mempool' && blocks?.length" class="stats bottom left">
|
||||
<p [innerHTML]="blocks[blockIndex].size | bytes: 2"></p>
|
||||
<p class="label" i18n="clock.block-size">block size</p>
|
||||
<p class="label" i18n="block.size">Size</p>
|
||||
</div>
|
||||
<div *ngIf="mode !== 'mempool' && blocks?.length" class="stats bottom right">
|
||||
<p class="force-wrap">
|
||||
<ng-container *ngTemplateOutlet="blocks[blockIndex].tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: blocks[blockIndex].tx_count | number}"></ng-container>
|
||||
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} <span class="label">transaction</span></ng-template>
|
||||
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} <span class="label">transactions</span></ng-template>
|
||||
{{ blocks[blockIndex].tx_count | number }}
|
||||
<span class="label" i18n="dashboard.txs">Transactions</span>
|
||||
</p>
|
||||
</div>
|
||||
<ng-container *ngIf="mempoolInfo$ | async as mempoolInfo;">
|
||||
<div *ngIf="mode === 'mempool'" class="stats bottom left">
|
||||
<p [innerHTML]="mempoolInfo.usage | bytes: 0"></p>
|
||||
<p class="label" i18n="dashboard.memory-usage|Memory usage">memory usage</p>
|
||||
<p class="label" i18n="dashboard.memory-usage|Memory usage">Memory Usage</p>
|
||||
</div>
|
||||
<div *ngIf="mode === 'mempool'" class="stats bottom right">
|
||||
<p>{{ mempoolInfo.size | number }}</p>
|
||||
<p class="label" i18n="dashboard.unconfirmed|Unconfirmed count">unconfirmed</p>
|
||||
<p class="label" i18n="dashboard.unconfirmed|Unconfirmed count">Unconfirmed</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
.label {
|
||||
font-size: calc(0.04 * var(--clock-width));
|
||||
line-height: calc(0.05 * var(--clock-width));
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
&.top {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
|
||||
<div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text" [ngStyle]="{'color': epochData.colorAdjustments}">
|
||||
<div class="card-text" [ngStyle]="{'color': epochData.colorAdjustments}">
|
||||
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
|
||||
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
|
||||
</span>
|
||||
@@ -24,9 +24,6 @@
|
||||
{{ epochData.change | absolute | number: '1.2-2' }}
|
||||
<span class="symbol">%</span>
|
||||
</div>
|
||||
<ng-template #recentlyAdjusted>
|
||||
<div class="card-text">—</div>
|
||||
</ng-template>
|
||||
<div class="symbol">
|
||||
<span i18n="difficulty-box.previous">Previous</span>:
|
||||
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">
|
||||
@@ -47,20 +44,30 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item" *ngIf="showHalving">
|
||||
<h5 class="card-title" i18n="difficulty-box.next-halving" i18n-ngbTooltip="difficulty-box.next-halving"
|
||||
ngbTooltip="Next Halving" placement="bottom" #averagefee [disableTooltip]="!isEllipsisActive(averagefee)">Next Halving</h5>
|
||||
<div class="card-text">
|
||||
<ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
|
||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
|
||||
<h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="mining.average-fee" [ngbTooltip]="halvingBlocksLeft" [tooltipContext]="{ epochData: epochData }" placement="bottom">
|
||||
<span>{{ timeUntilHalving | date }}</span>
|
||||
<div class="symbol" *ngIf="blocksUntilHalving === 1; else approxTime">
|
||||
<app-time kind="until" [time]="epochData.adjustedTimeAvg + now" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
|
||||
</div>
|
||||
<ng-template #approxTime>
|
||||
<div class="symbol">
|
||||
<app-time kind="until" [time]="timeUntilHalving" [fastRender]="false" [fixedRender]="true" [precision]="0" [numUnits]="2" [units]="['year', 'day', 'hour', 'minute']"></app-time>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="symbol"><app-time kind="until" [time]="epochData.timeUntilHalving" [fastRender]="true" [precision]="1"></app-time></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #halvingBlocksLeft let-epochData="epochData">
|
||||
<ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
|
||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #loadingDifficulty>
|
||||
<div class="difficulty-skeleton loading-container">
|
||||
<div class="item">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { combineLatest, Observable, timer } from 'rxjs';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { combineLatest, Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
interface EpochProgress {
|
||||
@@ -15,6 +15,8 @@ interface EpochProgress {
|
||||
previousRetarget: number;
|
||||
blocksUntilHalving: number;
|
||||
timeUntilHalving: number;
|
||||
timeAvg: number;
|
||||
adjustedTimeAvg: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -26,6 +28,9 @@ interface EpochProgress {
|
||||
export class DifficultyMiningComponent implements OnInit {
|
||||
isLoadingWebSocket$: Observable<boolean>;
|
||||
difficultyEpoch$: Observable<EpochProgress>;
|
||||
blocksUntilHalving: number | null = null;
|
||||
timeUntilHalving = 0;
|
||||
now = new Date().getTime();
|
||||
|
||||
@Input() showProgress = true;
|
||||
@Input() showHalving = false;
|
||||
@@ -64,8 +69,9 @@ export class DifficultyMiningComponent implements OnInit {
|
||||
colorPreviousAdjustments = '#ffffff66';
|
||||
}
|
||||
|
||||
const blocksUntilHalving = 210000 - (maxHeight % 210000);
|
||||
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
|
||||
this.blocksUntilHalving = 210000 - (maxHeight % 210000);
|
||||
this.timeUntilHalving = new Date().getTime() + (this.blocksUntilHalving * 600000);
|
||||
this.now = new Date().getTime();
|
||||
|
||||
const data = {
|
||||
base: `${da.progressPercent.toFixed(2)}%`,
|
||||
@@ -77,8 +83,10 @@ export class DifficultyMiningComponent implements OnInit {
|
||||
newDifficultyHeight: da.nextRetargetHeight,
|
||||
estimatedRetargetDate: da.estimatedRetargetDate,
|
||||
previousRetarget: da.previousRetarget,
|
||||
blocksUntilHalving,
|
||||
timeUntilHalving,
|
||||
blocksUntilHalving: this.blocksUntilHalving,
|
||||
timeUntilHalving: this.timeUntilHalving,
|
||||
timeAvg: da.timeAvg,
|
||||
adjustedTimeAvg: da.adjustedTimeAvg,
|
||||
};
|
||||
return data;
|
||||
})
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<div class="symbol" i18n="difficulty-box.average-block-time">Average block time</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text bigger" [ngStyle]="{'color': epochData.colorAdjustments}">
|
||||
<div class="card-text bigger" [ngStyle]="{'color': epochData.colorAdjustments}">
|
||||
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
|
||||
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
|
||||
</span>
|
||||
@@ -52,9 +52,6 @@
|
||||
{{ epochData.change | absolute | number: '1.2-2' }}
|
||||
<span class="symbol">%</span>
|
||||
</div>
|
||||
<ng-template #recentlyAdjusted>
|
||||
<div class="card-text">—</div>
|
||||
</ng-template>
|
||||
<div class="symbol">
|
||||
<span i18n="difficulty-box.previous">Previous</span>:
|
||||
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">
|
||||
|
||||
@@ -19,6 +19,7 @@ interface EpochProgress {
|
||||
blocksUntilHalving: number;
|
||||
timeUntilHalving: number;
|
||||
timeAvg: number;
|
||||
adjustedTimeAvg: number;
|
||||
}
|
||||
|
||||
type BlockStatus = 'mined' | 'behind' | 'ahead' | 'next' | 'remaining';
|
||||
@@ -153,6 +154,7 @@ export class DifficultyComponent implements OnInit {
|
||||
blocksUntilHalving,
|
||||
timeUntilHalving,
|
||||
timeAvg: da.timeAvg,
|
||||
adjustedTimeAvg: da.adjustedTimeAvg,
|
||||
};
|
||||
return data;
|
||||
})
|
||||
@@ -194,7 +196,7 @@ export class DifficultyComponent implements OnInit {
|
||||
|
||||
@HostListener('pointerdown', ['$event'])
|
||||
onPointerDown(event): void {
|
||||
if (this.epochSvgElement.nativeElement?.contains(event.target)) {
|
||||
if (this.epochSvgElement?.nativeElement?.contains(event.target)) {
|
||||
this.onPointerMove(event);
|
||||
event.preventDefault();
|
||||
}
|
||||
@@ -202,7 +204,7 @@ export class DifficultyComponent implements OnInit {
|
||||
|
||||
@HostListener('pointermove', ['$event'])
|
||||
onPointerMove(event): void {
|
||||
if (this.epochSvgElement.nativeElement?.contains(event.target)) {
|
||||
if (this.epochSvgElement?.nativeElement?.contains(event.target)) {
|
||||
this.tooltipPosition = { x: event.clientX, y: event.clientY };
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<div class="blocks" [class.wrap]="wrapBlocks">
|
||||
<ng-container *ngFor="let i of blockIndices">
|
||||
<div class="block-wrapper" [style]="wrapperStyle">
|
||||
<div class="block-container" [style]="containerStyle">
|
||||
<app-block-overview-graph
|
||||
#blockGraph
|
||||
[isLoading]="false"
|
||||
[resolution]="resolution"
|
||||
[blockLimit]="stateService.blockVSize"
|
||||
[orientation]="'top'"
|
||||
[flip]="false"
|
||||
[animationDuration]="animationDuration"
|
||||
[animationOffset]="animationOffset"
|
||||
[disableSpinner]="true"
|
||||
(txClickEvent)="onTxClick($event)"
|
||||
></app-block-overview-graph>
|
||||
<div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange>
|
||||
<h1 class="height">{{ blockInfo[i].height }}</h1>
|
||||
<h2 class="mined-by">by {{ blockInfo[i].extras.pool.name || 'Unknown' }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -0,0 +1,69 @@
|
||||
.blocks {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
align-content: flex-start;
|
||||
|
||||
&.wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.block-wrapper {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
--block-width: 1080px;
|
||||
|
||||
.info {
|
||||
position: absolute;
|
||||
left: 8%;
|
||||
top: 8%;
|
||||
right: 8%;
|
||||
bottom: 8%;
|
||||
height: 84%;
|
||||
width: 84%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: calc(var(--block-width) * 0.03);
|
||||
text-shadow: 0 0 calc(var(--block-width) * 0.05) black;
|
||||
|
||||
h1 {
|
||||
font-size: 6em;
|
||||
line-height: 1;
|
||||
margin-bottom: calc(var(--block-width) * 0.03);
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.8em;
|
||||
line-height: 1;
|
||||
margin-bottom: calc(var(--block-width) * 0.03);
|
||||
}
|
||||
|
||||
.hash {
|
||||
font-family: monospace;
|
||||
word-wrap: break-word;
|
||||
font-size: 1.4em;
|
||||
line-height: 1;
|
||||
margin-bottom: calc(var(--block-width) * 0.03);
|
||||
}
|
||||
|
||||
.mined-by {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.block-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { catchError, startWith } from 'rxjs/operators';
|
||||
import { Subject, Subscription, of } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
|
||||
import { detectWebGL } from '../../shared/graphs.utils';
|
||||
import { animate, style, transition, trigger } from '@angular/animations';
|
||||
import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe';
|
||||
|
||||
function bestFitResolution(min, max, n): number {
|
||||
const target = (min + max) / 2;
|
||||
let bestScore = Infinity;
|
||||
let best = null;
|
||||
for (let i = min; i <= max; i++) {
|
||||
const remainder = (n % i);
|
||||
if (remainder < bestScore || (remainder === bestScore && (Math.abs(i - target) < Math.abs(best - target)))) {
|
||||
bestScore = remainder;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
interface BlockInfo extends BlockExtended {
|
||||
timeString: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-eight-blocks',
|
||||
templateUrl: './eight-blocks.component.html',
|
||||
styleUrls: ['./eight-blocks.component.scss'],
|
||||
animations: [
|
||||
trigger('infoChange', [
|
||||
transition(':enter', [
|
||||
style({ opacity: 0 }),
|
||||
animate('1000ms', style({ opacity: 1 })),
|
||||
]),
|
||||
transition(':leave', [
|
||||
animate('1000ms 500ms', style({ opacity: 0 }))
|
||||
])
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class EightBlocksComponent implements OnInit, OnDestroy {
|
||||
network = '';
|
||||
latestBlocks: BlockExtended[] = [];
|
||||
isLoadingTransactions = true;
|
||||
strippedTransactions: { [height: number]: TransactionStripped[] } = {};
|
||||
webGlEnabled = true;
|
||||
hoverTx: string | null = null;
|
||||
|
||||
blocksSubscription: Subscription;
|
||||
cacheBlocksSubscription: Subscription;
|
||||
networkChangedSubscription: Subscription;
|
||||
queryParamsSubscription: Subscription;
|
||||
graphChangeSubscription: Subscription;
|
||||
|
||||
numBlocks: number = 8;
|
||||
blockIndices: number[] = [...Array(8).keys()];
|
||||
autofit: boolean = false;
|
||||
padding: number = 0;
|
||||
wrapBlocks: boolean = false;
|
||||
blockWidth: number = 1080;
|
||||
animationDuration: number = 2000;
|
||||
animationOffset: number = 0;
|
||||
stagger: number = 0;
|
||||
testing: boolean = true;
|
||||
testHeight: number = 800000;
|
||||
testShiftTimeout: number;
|
||||
|
||||
showInfo: boolean = true;
|
||||
blockInfo: BlockInfo[] = [];
|
||||
|
||||
wrapperStyle = {
|
||||
'--block-width': '1080px',
|
||||
width: '1080px',
|
||||
maxWidth: '1080px',
|
||||
padding: '',
|
||||
};
|
||||
containerStyle = {};
|
||||
resolution: number = 86;
|
||||
|
||||
@ViewChildren('blockGraph') blockGraphs: QueryList<BlockOverviewGraphComponent>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
public stateService: StateService,
|
||||
private websocketService: WebsocketService,
|
||||
private apiService: ApiService,
|
||||
private bytesPipe: BytesPipe,
|
||||
) {
|
||||
this.webGlEnabled = detectWebGL();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks']);
|
||||
this.network = this.stateService.network;
|
||||
|
||||
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
|
||||
this.numBlocks = Number.isInteger(Number(params.numBlocks)) ? Number(params.numBlocks) : 8;
|
||||
this.blockIndices = [...Array(this.numBlocks).keys()];
|
||||
this.autofit = params.autofit !== 'false';
|
||||
this.padding = Number.isInteger(Number(params.padding)) ? Number(params.padding) : 10;
|
||||
this.blockWidth = Number.isInteger(Number(params.blockWidth)) ? Number(params.blockWidth) : 540;
|
||||
this.wrapBlocks = params.wrap !== 'false';
|
||||
this.stagger = Number.isInteger(Number(params.stagger)) ? Number(params.stagger) : 0;
|
||||
this.animationDuration = Number.isInteger(Number(params.animationDuration)) ? Number(params.animationDuration) : 2000;
|
||||
this.animationOffset = this.padding * 2;
|
||||
|
||||
if (this.autofit) {
|
||||
this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2);
|
||||
} else {
|
||||
this.resolution = 86;
|
||||
}
|
||||
|
||||
this.wrapperStyle = {
|
||||
'--block-width': this.blockWidth + 'px',
|
||||
width: this.blockWidth + 'px',
|
||||
maxWidth: this.blockWidth + 'px',
|
||||
padding: (this.padding || 0) +'px 0px',
|
||||
};
|
||||
|
||||
if (params.test === 'true') {
|
||||
if (this.blocksSubscription) {
|
||||
this.blocksSubscription.unsubscribe();
|
||||
}
|
||||
this.blocksSubscription = (new Subject<BlockExtended[]>()).subscribe((blocks) => {
|
||||
this.handleNewBlock(blocks.slice(0, this.numBlocks));
|
||||
});
|
||||
this.shiftTestBlocks();
|
||||
} else if (!this.blocksSubscription) {
|
||||
this.blocksSubscription = this.stateService.blocks$
|
||||
.subscribe((blocks) => {
|
||||
this.handleNewBlock(blocks.slice(0, this.numBlocks));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.setupBlockGraphs();
|
||||
|
||||
this.networkChangedSubscription = this.stateService.networkChanged$
|
||||
.subscribe((network) => this.network = network);
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.graphChangeSubscription = this.blockGraphs.changes.pipe(startWith(null)).subscribe(() => {
|
||||
this.setupBlockGraphs();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.stateService.markBlock$.next({});
|
||||
if (this.blocksSubscription) {
|
||||
this.blocksSubscription?.unsubscribe();
|
||||
}
|
||||
this.cacheBlocksSubscription?.unsubscribe();
|
||||
this.networkChangedSubscription?.unsubscribe();
|
||||
this.queryParamsSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
shiftTestBlocks(): void {
|
||||
const sub = this.apiService.getBlocks$(this.testHeight).subscribe(result => {
|
||||
sub.unsubscribe();
|
||||
this.handleNewBlock(result.slice(0, this.numBlocks));
|
||||
this.testHeight++;
|
||||
clearTimeout(this.testShiftTimeout);
|
||||
this.testShiftTimeout = window.setTimeout(() => { this.shiftTestBlocks(); }, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
async handleNewBlock(blocks: BlockExtended[]): Promise<void> {
|
||||
const readyPromises: Promise<TransactionStripped[]>[] = [];
|
||||
const previousBlocks = this.latestBlocks;
|
||||
const newHeights = {};
|
||||
this.latestBlocks = blocks;
|
||||
for (const block of blocks) {
|
||||
newHeights[block.height] = true;
|
||||
if (!this.strippedTransactions[block.height]) {
|
||||
readyPromises.push(new Promise((resolve) => {
|
||||
const subscription = this.apiService.getStrippedBlockTransactions$(block.id).pipe(
|
||||
catchError(() => {
|
||||
return of([]);
|
||||
}),
|
||||
).subscribe((transactions) => {
|
||||
this.strippedTransactions[block.height] = transactions;
|
||||
subscription.unsubscribe();
|
||||
resolve(transactions);
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
await Promise.allSettled(readyPromises);
|
||||
this.updateBlockGraphs(blocks);
|
||||
|
||||
// free up old transactions
|
||||
previousBlocks.forEach(block => {
|
||||
if (!newHeights[block.height]) {
|
||||
delete this.strippedTransactions[block.height];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateBlockGraphs(blocks): void {
|
||||
const startTime = performance.now() + 1000 - (this.stagger < 0 ? this.stagger * 8 : 0);
|
||||
if (this.blockGraphs) {
|
||||
this.blockGraphs.forEach((graph, index) => {
|
||||
graph.replace(this.strippedTransactions[blocks?.[index]?.height] || [], 'right', false, startTime + (this.stagger * index));
|
||||
});
|
||||
}
|
||||
this.showInfo = false;
|
||||
setTimeout(() => {
|
||||
this.blockInfo = blocks.map(block => {
|
||||
return {
|
||||
...block,
|
||||
timeString: (new Date(block.timestamp * 1000)).toLocaleTimeString(),
|
||||
};
|
||||
});
|
||||
this.showInfo = true;
|
||||
}, 1600); // Should match the animation time.
|
||||
}
|
||||
|
||||
setupBlockGraphs(): void {
|
||||
if (this.blockGraphs) {
|
||||
this.blockGraphs.forEach((graph, index) => {
|
||||
graph.destroy();
|
||||
graph.setup(this.strippedTransactions[this.latestBlocks?.[index]?.height] || []);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
|
||||
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
|
||||
if (!event.keyModifier) {
|
||||
this.router.navigate([url]);
|
||||
} else {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
onTxHover(txid: string): void {
|
||||
if (txid && txid.length) {
|
||||
this.hoverTx = txid;
|
||||
} else {
|
||||
this.hoverTx = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.fee-distribution-chart {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OnChanges, OnDestroy } from '@angular/core';
|
||||
import { HostListener, OnChanges, OnDestroy } from '@angular/core';
|
||||
import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { TransactionStripped } from '../../interfaces/websocket.interface';
|
||||
import { StateService } from '../../services/state.service';
|
||||
@@ -9,6 +9,7 @@ import { Subscription } from 'rxjs';
|
||||
@Component({
|
||||
selector: 'app-fee-distribution-graph',
|
||||
templateUrl: './fee-distribution-graph.component.html',
|
||||
styleUrls: ['./fee-distribution-graph.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@@ -25,6 +26,7 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
|
||||
simple: boolean = false;
|
||||
data: number[][];
|
||||
labelInterval: number = 50;
|
||||
smallScreen: boolean = window.innerWidth < 450;
|
||||
|
||||
rateUnitSub: Subscription;
|
||||
weightMode: boolean = false;
|
||||
@@ -95,9 +97,9 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
|
||||
this.mempoolVsizeFeesOptions = {
|
||||
grid: {
|
||||
height: '210',
|
||||
right: '20',
|
||||
right: this.smallScreen ? '10' : '20',
|
||||
top: '22',
|
||||
left: '40',
|
||||
left: this.smallScreen ? '10' : '40',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
@@ -131,16 +133,17 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
show: true,
|
||||
show: !this.smallScreen,
|
||||
formatter: (value: number): string => {
|
||||
const unitValue = this.weightMode ? value / 4 : value;
|
||||
const selectedPowerOfTen = selectPowerOfTen(unitValue);
|
||||
const newVal = Math.round(unitValue / selectedPowerOfTen.divider);
|
||||
const scaledValue = unitValue / selectedPowerOfTen.divider;
|
||||
const newVal = scaledValue >= 100 ? Math.round(scaledValue) : scaledValue.toPrecision(3);
|
||||
return `${newVal}${selectedPowerOfTen.unit}`;
|
||||
},
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
show: !this.smallScreen,
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
@@ -151,11 +154,13 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
|
||||
position: 'top',
|
||||
color: '#ffffff',
|
||||
textShadowBlur: 0,
|
||||
fontSize: this.smallScreen ? 10 : 12,
|
||||
formatter: (label: { data: number[] }): string => {
|
||||
const value = label.data[1];
|
||||
const unitValue = this.weightMode ? value / 4 : value;
|
||||
const selectedPowerOfTen = selectPowerOfTen(unitValue);
|
||||
const newVal = Math.round(unitValue / selectedPowerOfTen.divider);
|
||||
const scaledValue = unitValue / selectedPowerOfTen.divider;
|
||||
const newVal = scaledValue >= 100 ? Math.round(scaledValue) : scaledValue.toPrecision(3);
|
||||
return `${newVal}${selectedPowerOfTen.unit}`;
|
||||
}
|
||||
},
|
||||
@@ -179,6 +184,16 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
|
||||
};
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(): void {
|
||||
const isSmallScreen = window.innerWidth < 450;
|
||||
if (this.smallScreen !== isSmallScreen) {
|
||||
this.smallScreen = isSmallScreen;
|
||||
this.prepareChart();
|
||||
this.mountChart();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.rateUnitSub.unsubscribe();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="progress inc-tx-progress-bar">
|
||||
<div class="progress-bar" role="progressbar" [ngStyle]="{'width': mempoolInfoData.progressWidth, 'background-color': mempoolInfoData.progressColor}"> </div>
|
||||
<div class="progress-text" *only-vsize>‎{{ mempoolInfoData.vBytesPerSecond | ceil | number }} <ng-container i18n="shared.vbytes-per-second|vB/s">vB/s</ng-container></div>
|
||||
<div class="progress-text" *only-weight>‎{{ mempoolInfoData.vBytesPerSecond * 4 | ceil | number }} <ng-container i18n="shared.weight-units-per-second|vB/s">WU/s</ng-container></div>
|
||||
<div class="progress-text" *only-weight>‎{{ mempoolInfoData.vBytesPerSecond * 4 | ceil | number }} <ng-container i18n="shared.weight-per-second|WU/s">WU/s</ng-container></div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<a class="dropdown-item" routerLinkActive="active"
|
||||
[routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" i18n="mining.block-sizes-weights">Block Sizes and Weights</a>
|
||||
<a *ngIf="stateService.env.AUDIT" class="dropdown-item" routerLinkActive="active"
|
||||
[routerLink]="['/graphs/mining/block-health' | relativeUrl]" i18n="mining.block-health">Block Health</a>
|
||||
[routerLink]="['/graphs/mining/block-health' | relativeUrl]" i18n="mining.blocks-health">Block Health</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'" [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
|
||||
@@ -57,8 +57,6 @@
|
||||
}
|
||||
.chart-widget {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: 240px;
|
||||
}
|
||||
|
||||
.pool-distribution {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { merge, Observable, of } from 'rxjs';
|
||||
import { echarts, EChartsOption } from '../../graphs/echarts';
|
||||
import { combineLatest, fromEvent, merge, Observable, of } from 'rxjs';
|
||||
import { map, mergeMap, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
@@ -31,6 +31,7 @@ import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
export class HashrateChartComponent implements OnInit {
|
||||
@Input() tableOnly = false;
|
||||
@Input() widget = false;
|
||||
@Input() height: number = 300;
|
||||
@Input() right: number | string = 45;
|
||||
@Input() left: number | string = 75;
|
||||
|
||||
@@ -86,28 +87,32 @@ export class HashrateChartComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
|
||||
this.hashrateObservable$ = merge(
|
||||
this.radioGroupForm.get('dateSpan').valueChanges
|
||||
.pipe(
|
||||
startWith(this.radioGroupForm.controls.dateSpan.value),
|
||||
switchMap((timespan) => {
|
||||
if (!this.widget && !firstRun) {
|
||||
this.storageService.setValue('miningWindowPreference', timespan);
|
||||
}
|
||||
this.timespan = timespan;
|
||||
firstRun = false;
|
||||
this.miningWindowPreference = timespan;
|
||||
this.isLoading = true;
|
||||
return this.apiService.getHistoricalHashrate$(this.timespan);
|
||||
})
|
||||
),
|
||||
this.stateService.chainTip$
|
||||
this.hashrateObservable$ = combineLatest(
|
||||
merge(
|
||||
this.radioGroupForm.get('dateSpan').valueChanges
|
||||
.pipe(
|
||||
switchMap(() => {
|
||||
startWith(this.radioGroupForm.controls.dateSpan.value),
|
||||
switchMap((timespan) => {
|
||||
if (!this.widget && !firstRun) {
|
||||
this.storageService.setValue('miningWindowPreference', timespan);
|
||||
}
|
||||
this.timespan = timespan;
|
||||
firstRun = false;
|
||||
this.miningWindowPreference = timespan;
|
||||
this.isLoading = true;
|
||||
return this.apiService.getHistoricalHashrate$(this.timespan);
|
||||
})
|
||||
)
|
||||
),
|
||||
this.stateService.chainTip$
|
||||
.pipe(
|
||||
switchMap(() => {
|
||||
return this.apiService.getHistoricalHashrate$(this.timespan);
|
||||
})
|
||||
)
|
||||
),
|
||||
fromEvent(window, 'resize').pipe(startWith(null)),
|
||||
).pipe(
|
||||
map(([response, _]) => response),
|
||||
tap((response: any) => {
|
||||
const data = response.body;
|
||||
|
||||
@@ -204,7 +209,7 @@ export class HashrateChartComponent implements OnInit {
|
||||
title: title,
|
||||
animation: false,
|
||||
color: [
|
||||
new graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||
new echarts.graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||
{ offset: 0, color: '#F4511E99' },
|
||||
{ offset: 0.25, color: '#FB8C0099' },
|
||||
{ offset: 0.5, color: '#FFB30099' },
|
||||
@@ -212,7 +217,7 @@ export class HashrateChartComponent implements OnInit {
|
||||
{ offset: 1, color: '#7CB34299' }
|
||||
]),
|
||||
'#D81B60',
|
||||
new graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||
new echarts.graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||
{ offset: 0, color: '#F4511E' },
|
||||
{ offset: 0.25, color: '#FB8C00' },
|
||||
{ offset: 0.5, color: '#FFB300' },
|
||||
@@ -221,6 +226,7 @@ export class HashrateChartComponent implements OnInit {
|
||||
]),
|
||||
],
|
||||
grid: {
|
||||
height: (this.widget && this.height) ? this.height - 30 : undefined,
|
||||
top: this.widget ? 20 : 40,
|
||||
bottom: this.widget ? 30 : 70,
|
||||
right: this.right,
|
||||
@@ -249,10 +255,8 @@ export class HashrateChartComponent implements OnInit {
|
||||
for (const tick of ticks) {
|
||||
if (tick.seriesIndex === 0) { // Hashrate
|
||||
let hashrate = tick.data[1];
|
||||
if (this.isMobile()) {
|
||||
hashratePowerOfTen = selectPowerOfTen(tick.data[1]);
|
||||
hashrate = Math.round(tick.data[1] / hashratePowerOfTen.divider);
|
||||
}
|
||||
hashratePowerOfTen = selectPowerOfTen(tick.data[1], 10);
|
||||
hashrate = tick.data[1] / hashratePowerOfTen.divider;
|
||||
hashrateString = `${tick.marker} ${tick.seriesName}: ${formatNumber(hashrate, this.locale, '1.0-0')} ${hashratePowerOfTen.unit}H/s<br>`;
|
||||
} else if (tick.seriesIndex === 1) { // Difficulty
|
||||
let difficultyPowerOfTen = hashratePowerOfTen;
|
||||
@@ -260,18 +264,14 @@ export class HashrateChartComponent implements OnInit {
|
||||
if (difficulty === null) {
|
||||
difficultyString = `${tick.marker} ${tick.seriesName}: No data<br>`;
|
||||
} else {
|
||||
if (this.isMobile()) {
|
||||
difficultyPowerOfTen = selectPowerOfTen(tick.data[1]);
|
||||
difficulty = Math.round(tick.data[1] / difficultyPowerOfTen.divider);
|
||||
}
|
||||
difficultyPowerOfTen = selectPowerOfTen(tick.data[1]);
|
||||
difficulty = tick.data[1] / difficultyPowerOfTen.divider;
|
||||
difficultyString = `${tick.marker} ${tick.seriesName}: ${formatNumber(difficulty, this.locale, '1.2-2')} ${difficultyPowerOfTen.unit}<br>`;
|
||||
}
|
||||
} else if (tick.seriesIndex === 2) { // Hashrate MA
|
||||
let hashrate = tick.data[1];
|
||||
if (this.isMobile()) {
|
||||
hashratePowerOfTen = selectPowerOfTen(tick.data[1]);
|
||||
hashrate = Math.round(tick.data[1] / hashratePowerOfTen.divider);
|
||||
}
|
||||
hashratePowerOfTen = selectPowerOfTen(tick.data[1], 10);
|
||||
hashrate = tick.data[1] / hashratePowerOfTen.divider;
|
||||
hashrateStringMA = `${tick.marker} ${tick.seriesName}: ${formatNumber(hashrate, this.locale, '1.0-0')} ${hashratePowerOfTen.unit}H/s`;
|
||||
}
|
||||
}
|
||||
@@ -342,7 +342,7 @@ export class HashrateChartComponent implements OnInit {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: 'rgb(110, 112, 121)',
|
||||
formatter: (val) => {
|
||||
formatter: (val): string => {
|
||||
const selectedPowerOfTen: any = selectPowerOfTen(val);
|
||||
const newVal = Math.round(val / selectedPowerOfTen.divider);
|
||||
return `${newVal} ${selectedPowerOfTen.unit}H/s`;
|
||||
@@ -364,9 +364,9 @@ export class HashrateChartComponent implements OnInit {
|
||||
position: 'right',
|
||||
axisLabel: {
|
||||
color: 'rgb(110, 112, 121)',
|
||||
formatter: (val) => {
|
||||
formatter: (val): string => {
|
||||
if (this.stateService.network === 'signet') {
|
||||
return val;
|
||||
return `${val}`;
|
||||
}
|
||||
const selectedPowerOfTen: any = selectPowerOfTen(val);
|
||||
const newVal = Math.round(val / selectedPowerOfTen.divider);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { EChartsOption } from '../../graphs/echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { delay, map, retryWhen, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
@@ -11,6 +11,13 @@ import { MiningService } from '../../services/mining.service';
|
||||
import { download } from '../../shared/graphs.utils';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
interface Hashrate {
|
||||
timestamp: number;
|
||||
avgHashRate: number;
|
||||
share: number;
|
||||
poolName: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-hashrate-chart-pools',
|
||||
templateUrl: './hashrate-chart-pools.component.html',
|
||||
@@ -32,6 +39,7 @@ export class HashrateChartPoolsComponent implements OnInit {
|
||||
miningWindowPreference: string;
|
||||
radioGroupForm: UntypedFormGroup;
|
||||
|
||||
hashrates: Hashrate[];
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
@@ -87,56 +95,9 @@ export class HashrateChartPoolsComponent implements OnInit {
|
||||
return this.apiService.getHistoricalPoolsHashrate$(timespan)
|
||||
.pipe(
|
||||
tap((response) => {
|
||||
const hashrates = response.body;
|
||||
this.hashrates = response.body;
|
||||
// Prepare series (group all hashrates data point by pool)
|
||||
const grouped = {};
|
||||
for (const hashrate of hashrates) {
|
||||
if (!grouped.hasOwnProperty(hashrate.poolName)) {
|
||||
grouped[hashrate.poolName] = [];
|
||||
}
|
||||
grouped[hashrate.poolName].push(hashrate);
|
||||
}
|
||||
|
||||
const series = [];
|
||||
const legends = [];
|
||||
for (const name in grouped) {
|
||||
series.push({
|
||||
zlevel: 0,
|
||||
stack: 'Total',
|
||||
name: name,
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
data: grouped[name].map((val) => [val.timestamp * 1000, val.share * 100]),
|
||||
type: 'line',
|
||||
lineStyle: { width: 0 },
|
||||
areaStyle: { opacity: 1 },
|
||||
smooth: true,
|
||||
color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, '').toLowerCase()],
|
||||
emphasis: {
|
||||
disabled: true,
|
||||
scale: false,
|
||||
},
|
||||
});
|
||||
|
||||
legends.push({
|
||||
name: name,
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
},
|
||||
icon: 'roundRect',
|
||||
itemStyle: {
|
||||
color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.prepareChartOptions({
|
||||
legends: legends,
|
||||
series: series,
|
||||
});
|
||||
this.isLoading = false;
|
||||
|
||||
const series = this.applyHashrates();
|
||||
if (series.length === 0) {
|
||||
this.cd.markForCheck();
|
||||
throw new Error();
|
||||
@@ -156,6 +117,77 @@ export class HashrateChartPoolsComponent implements OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
applyHashrates(): any[] {
|
||||
const times: { [time: number]: { hashrates: { [pool: string]: Hashrate } } } = {};
|
||||
const pools = {};
|
||||
for (const hashrate of this.hashrates) {
|
||||
if (!times[hashrate.timestamp]) {
|
||||
times[hashrate.timestamp] = { hashrates: {} };
|
||||
}
|
||||
times[hashrate.timestamp].hashrates[hashrate.poolName] = hashrate;
|
||||
if (!pools[hashrate.poolName]) {
|
||||
pools[hashrate.poolName] = true;
|
||||
}
|
||||
}
|
||||
|
||||
const sortedTimes = Object.keys(times).sort((a,b) => parseInt(a) - parseInt(b)).map(time => ({ time: parseInt(time), hashrates: times[time].hashrates }));
|
||||
const lastHashrates = sortedTimes[sortedTimes.length - 1].hashrates;
|
||||
const sortedPools = Object.keys(pools).sort((a,b) => {
|
||||
if (lastHashrates[b]?.share ?? lastHashrates[a]?.share ?? false) {
|
||||
// sort by descending share of hashrate in latest period
|
||||
return (lastHashrates[b]?.share || 0) - (lastHashrates[a]?.share || 0);
|
||||
} else {
|
||||
// tiebreak by pool name
|
||||
b < a;
|
||||
}
|
||||
});
|
||||
|
||||
const series = [];
|
||||
const legends = [];
|
||||
for (const name of sortedPools) {
|
||||
const data = sortedTimes.map(({ time, hashrates }) => {
|
||||
return [time * 1000, (hashrates[name]?.share || 0) * 100];
|
||||
});
|
||||
series.push({
|
||||
zlevel: 0,
|
||||
stack: 'Total',
|
||||
name: name,
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
data,
|
||||
type: 'line',
|
||||
lineStyle: { width: 0 },
|
||||
areaStyle: { opacity: 1 },
|
||||
smooth: true,
|
||||
color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, '').toLowerCase()],
|
||||
emphasis: {
|
||||
disabled: true,
|
||||
scale: false,
|
||||
},
|
||||
});
|
||||
|
||||
legends.push({
|
||||
name: name,
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
},
|
||||
icon: 'roundRect',
|
||||
itemStyle: {
|
||||
color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.prepareChartOptions({
|
||||
legends: legends,
|
||||
series: series,
|
||||
});
|
||||
this.isLoading = false;
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
prepareChartOptions(data) {
|
||||
let title: object;
|
||||
if (data.series.length === 0) {
|
||||
@@ -256,6 +288,7 @@ export class HashrateChartPoolsComponent implements OnInit {
|
||||
},
|
||||
}],
|
||||
};
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onChartInit(ec) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, Input, Inject, LOCALE_ID, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { EChartsOption } from '../../graphs/echarts';
|
||||
import { OnChanges } from '@angular/core';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
import { download, formatterXAxis, formatterXAxisLabel } from '../../shared/graphs.utils';
|
||||
@@ -7,6 +7,8 @@ import { formatNumber } from '@angular/common';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
const OUTLIERS_MEDIAN_MULTIPLIER = 4;
|
||||
|
||||
@Component({
|
||||
selector: 'app-incoming-transactions-graph',
|
||||
templateUrl: './incoming-transactions-graph.component.html',
|
||||
@@ -29,6 +31,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
|
||||
@Input() left: number | string = '0';
|
||||
@Input() template: ('widget' | 'advanced') = 'widget';
|
||||
@Input() windowPreferenceOverride: string;
|
||||
@Input() outlierCappingEnabled: boolean = false;
|
||||
|
||||
isLoading = true;
|
||||
mempoolStatsChartOption: EChartsOption = {};
|
||||
@@ -37,8 +40,10 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
|
||||
};
|
||||
windowPreference: string;
|
||||
chartInstance: any = undefined;
|
||||
MA: number[][] = [];
|
||||
weightMode: boolean = false;
|
||||
rateUnitSub: Subscription;
|
||||
medianVbytesPerSecond: number | undefined;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
@@ -62,17 +67,107 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
|
||||
return;
|
||||
}
|
||||
this.windowPreference = this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference');
|
||||
const windowSize = Math.max(10, Math.floor(this.data.series[0].length / 8));
|
||||
this.MA = this.calculateMA(this.data.series[0], windowSize);
|
||||
if (this.outlierCappingEnabled === true) {
|
||||
this.computeMedianVbytesPerSecond(this.data.series[0]);
|
||||
}
|
||||
this.mountChart();
|
||||
}
|
||||
|
||||
rendered() {
|
||||
if (!this.data) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the median value of the vbytes per second chart to hide outliers
|
||||
*/
|
||||
computeMedianVbytesPerSecond(data: number[][]): void {
|
||||
const vBytes: number[] = [];
|
||||
for (const value of data) {
|
||||
vBytes.push(value[1]);
|
||||
}
|
||||
const sorted = vBytes.slice().sort((a, b) => a - b);
|
||||
const middle = Math.floor(sorted.length / 2);
|
||||
this.medianVbytesPerSecond = sorted[middle];
|
||||
if (sorted.length % 2 === 0) {
|
||||
this.medianVbytesPerSecond = (sorted[middle - 1] + sorted[middle]) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
/// calculate the moving average of the provided data based on windowSize
|
||||
calculateMA(data: number[][], windowSize: number = 100): number[][] {
|
||||
//update const variables that are not changed
|
||||
const ma: number[][] = [];
|
||||
let sum = 0;
|
||||
let i = 0;
|
||||
|
||||
//calculate the centered moving average
|
||||
for (i = 0; i < data.length; i++) {
|
||||
sum += data[i][1];
|
||||
if (i >= windowSize) {
|
||||
sum -= data[i - windowSize][1];
|
||||
const midpoint = i - Math.floor(windowSize / 2);
|
||||
const avg = sum / windowSize;
|
||||
ma.push([data[midpoint][0], avg]);
|
||||
}
|
||||
}
|
||||
|
||||
//return the moving average array
|
||||
return ma;
|
||||
}
|
||||
|
||||
mountChart(): void {
|
||||
//create an array for the echart series
|
||||
//similar to how it is done in mempool-graph.component.ts
|
||||
const seriesGraph = [];
|
||||
seriesGraph.push({
|
||||
zlevel: 0,
|
||||
name: 'data',
|
||||
data: this.data.series[0],
|
||||
type: 'line',
|
||||
smooth: false,
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
},
|
||||
markLine: {
|
||||
silent: true,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
color: '#fff',
|
||||
opacity: 1,
|
||||
width: 2,
|
||||
},
|
||||
data: [{
|
||||
yAxis: 1667,
|
||||
label: {
|
||||
show: false,
|
||||
color: '#ffffff',
|
||||
}
|
||||
}],
|
||||
}
|
||||
});
|
||||
if (this.template !== 'widget') {
|
||||
seriesGraph.push({
|
||||
zlevel: 0,
|
||||
name: 'MA',
|
||||
data: this.MA,
|
||||
type: 'line',
|
||||
smooth: false,
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: "white",
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.mempoolStatsChartOption = {
|
||||
grid: {
|
||||
height: this.height,
|
||||
@@ -114,27 +209,31 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
|
||||
obj[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 80;
|
||||
return obj;
|
||||
},
|
||||
extraCssText: `width: ${(['2h', '24h'].includes(this.windowPreference) || this.template === 'widget') ? '125px' : '135px'};
|
||||
background: transparent;
|
||||
extraCssText: `background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;`,
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
},
|
||||
formatter: (params: any) => {
|
||||
const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, params[0].axisValue);
|
||||
const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, params[0].axisValue);
|
||||
const colorSpan = (color: string) => `<span class="indicator" style="background-color: ` + color + `"></span>`;
|
||||
let itemFormatted = '<div class="title">' + axisValueLabel + '</div>';
|
||||
params.map((item: any, index: number) => {
|
||||
if (index < 26) {
|
||||
itemFormatted += `<div class="item">
|
||||
<div class="indicator-container">${colorSpan(item.color)}</div>
|
||||
<div class="grow"></div>
|
||||
<div class="value">${formatNumber(this.weightMode ? item.value[1] * 4 : item.value[1], this.locale, '1.0-0')} <span class="symbol">${this.weightMode ? 'WU' : 'vB'}/s</span></div>
|
||||
</div>`;
|
||||
|
||||
//Do no include MA in tooltip legend!
|
||||
if (item.seriesName !== 'MA') {
|
||||
if (index < 26) {
|
||||
itemFormatted += `<div class="item">
|
||||
<div class="indicator-container">${colorSpan(item.color)}</div>
|
||||
<div class="grow"></div>
|
||||
<div class="value">${formatNumber(item.value[1], this.locale, '1.0-0')}<span class="symbol">vB/s</span></div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
});
|
||||
return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}">${itemFormatted}</div>`;
|
||||
return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}"
|
||||
style="width: ${(this.windowPreference === '2h' || this.template === 'widget') ? '125px' : '215px'}">${itemFormatted}</div>`;
|
||||
}
|
||||
},
|
||||
xAxis: [
|
||||
@@ -156,6 +255,13 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
|
||||
}
|
||||
],
|
||||
yAxis: {
|
||||
max: (value) => {
|
||||
if (!this.outlierCappingEnabled || value.max < this.medianVbytesPerSecond * OUTLIERS_MEDIAN_MULTIPLIER) {
|
||||
return undefined;
|
||||
} else {
|
||||
return Math.round(this.medianVbytesPerSecond * OUTLIERS_MEDIAN_MULTIPLIER);
|
||||
}
|
||||
},
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
@@ -171,35 +277,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
zlevel: 0,
|
||||
data: this.data.series[0],
|
||||
type: 'line',
|
||||
smooth: false,
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
},
|
||||
markLine: {
|
||||
silent: true,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
color: '#fff',
|
||||
opacity: 1,
|
||||
width: 2,
|
||||
},
|
||||
data: [{
|
||||
yAxis: 1667,
|
||||
label: {
|
||||
show: false,
|
||||
color: '#ffffff',
|
||||
}
|
||||
}],
|
||||
}
|
||||
},
|
||||
],
|
||||
series: seriesGraph,
|
||||
visualMap: {
|
||||
show: false,
|
||||
top: 50,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, Inject, LOCALE_ID, ChangeDetectionStrategy, Input, OnChanges, OnInit } from '@angular/core';
|
||||
import { formatDate, formatNumber } from '@angular/common';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { EChartsOption } from '../../graphs/echarts';
|
||||
|
||||
@Component({
|
||||
selector: 'app-lbtc-pegs-graph',
|
||||
@@ -18,16 +18,15 @@ import { EChartsOption } from 'echarts';
|
||||
})
|
||||
export class LbtcPegsGraphComponent implements OnInit, OnChanges {
|
||||
@Input() data: any;
|
||||
@Input() height: number | string = '320';
|
||||
pegsChartOptions: EChartsOption;
|
||||
|
||||
height: number | string = '200';
|
||||
right: number | string = '10';
|
||||
top: number | string = '20';
|
||||
left: number | string = '50';
|
||||
template: ('widget' | 'advanced') = 'widget';
|
||||
isLoading = true;
|
||||
|
||||
pegsChartOption: EChartsOption = {};
|
||||
pegsChartInitOption = {
|
||||
renderer: 'svg'
|
||||
};
|
||||
@@ -41,20 +40,24 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
if (!this.data) {
|
||||
if (!this.data?.liquidPegs) {
|
||||
return;
|
||||
}
|
||||
this.pegsChartOptions = this.createChartOptions(this.data.series, this.data.labels);
|
||||
if (!this.data.liquidReserves) {
|
||||
this.pegsChartOptions = this.createChartOptions(this.data.liquidPegs.series, this.data.liquidPegs.labels);
|
||||
} else {
|
||||
this.pegsChartOptions = this.createChartOptions(this.data.liquidPegs.series, this.data.liquidPegs.labels, this.data.liquidReserves.series);
|
||||
}
|
||||
}
|
||||
|
||||
rendered() {
|
||||
if (!this.data) {
|
||||
if (!this.data.liquidPegs) {
|
||||
return;
|
||||
}
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
createChartOptions(series: number[], labels: string[]): EChartsOption {
|
||||
createChartOptions(pegSeries: number[], labels: string[], reservesSeries?: number[],): EChartsOption {
|
||||
return {
|
||||
grid: {
|
||||
height: this.height,
|
||||
@@ -99,17 +102,18 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
|
||||
type: 'line',
|
||||
},
|
||||
formatter: (params: any) => {
|
||||
const colorSpan = (color: string) => `<span class="indicator" style="background-color: #116761;"></span>`;
|
||||
const colorSpan = (color: string) => `<span class="indicator" style="background-color: ${color};"></span>`;
|
||||
let itemFormatted = '<div class="title">' + params[0].axisValue + '</div>';
|
||||
params.map((item: any, index: number) => {
|
||||
for (let index = params.length - 1; index >= 0; index--) {
|
||||
const item = params[index];
|
||||
if (index < 26) {
|
||||
itemFormatted += `<div class="item">
|
||||
<div class="indicator-container">${colorSpan(item.color)}</div>
|
||||
<div class="grow"></div>
|
||||
<div class="value">${formatNumber(item.value, this.locale, '1.2-2')} <span class="symbol">L-BTC</span></div>
|
||||
<div style="margin-right: 5px"></div>
|
||||
<div class="value">${formatNumber(item.value, this.locale, '1.2-2')} <span class="symbol">${item.seriesName}</span></div>
|
||||
</div>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}">${itemFormatted}</div>`;
|
||||
}
|
||||
},
|
||||
@@ -138,20 +142,34 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: series,
|
||||
data: pegSeries,
|
||||
name: 'L-BTC',
|
||||
color: '#116761',
|
||||
type: 'line',
|
||||
stack: 'total',
|
||||
smooth: false,
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
areaStyle: {
|
||||
opacity: 0.2,
|
||||
color: '#116761',
|
||||
},
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
width: 2,
|
||||
color: '#116761',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: reservesSeries,
|
||||
name: 'BTC',
|
||||
color: '#EA983B',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#EA983B',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<ng-container *ngIf="{ val: network$ | async } as network">
|
||||
<header>
|
||||
<header class="sticky-header">
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
||||
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
|
||||
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
|
||||
@@ -53,7 +53,7 @@
|
||||
<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 + (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 *ngIf="env.BISQ_ENABLED" [href]="env.BISQ_WEBSITE_URL + urlLanguage + (networkPaths['bisq'] || '')" 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>
|
||||
@@ -78,7 +78,7 @@
|
||||
<li class="nav-item" routerLinkActive="active" id="btn-assets">
|
||||
<a class="nav-link" [routerLink]="['/assets' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'database']" [fixedWidth]="true" i18n-title="master-page.assets" title="Assets"></fa-icon></a>
|
||||
</li>
|
||||
<li [hidden]="isMobile" class="nav-item mr-2" routerLinkActive="active" id="btn-docs">
|
||||
<li class="nav-item mr-2" routerLinkActive="active" id="btn-docs">
|
||||
<a class="nav-link" [routerLink]="['/docs' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'book']" [fixedWidth]="true" i18n-title="master-page.docs" title="Docs"></fa-icon></a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active" id="btn-about">
|
||||
@@ -98,4 +98,4 @@
|
||||
|
||||
<app-global-footer *ngIf="footerVisible"></app-global-footer>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
.sticky-header {
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
li.nav-item.active {
|
||||
background-color: #653b9c;
|
||||
}
|
||||
@@ -15,6 +23,11 @@ li.nav-item {
|
||||
margin: auto 10px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
@media (max-width: 429px) {
|
||||
margin: auto 5px;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<div [ngClass]="{'widget': widget, 'extra-margin-right': widget}">
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div style="min-height: 295px">
|
||||
<table class="table table-borderless">
|
||||
<thead style="vertical-align: middle;">
|
||||
<th class="address text-left" [ngClass]="{'widget': widget}" i18n="shared.address">Address</th>
|
||||
<th class="amount text-right" [ngClass]="{'widget': widget}" i18n="address.balance">Balance</th>
|
||||
</thead>
|
||||
<tbody *ngIf="federationAddresses$ | async as addresses; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
<ng-container *ngIf="widget; else regularRows">
|
||||
<tr *ngFor="let address of addresses | slice:0:5">
|
||||
<td class="address text-left widget">
|
||||
<a href="{{ env.MEMPOOL_WEBSITE_URL + '/address/' + address.bitcoinaddress }}" target="_blank" style="color:#b86d12">
|
||||
<app-truncate [text]="address.bitcoinaddress" [lastChars]="6"></app-truncate>
|
||||
</a>
|
||||
</td>
|
||||
<td class="amount text-right widget">
|
||||
<app-amount [satoshis]="+address.balance" [noFiat]="true" [forceBtc]="true"></app-amount>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template #regularRows>
|
||||
<tr *ngFor="let address of addresses | slice:(page - 1) * pageSize:page * pageSize">
|
||||
<td class="address text-left">
|
||||
<a href="{{ env.MEMPOOL_WEBSITE_URL + '/address/' + address.bitcoinaddress }}" target="_blank" style="color:#b86d12">
|
||||
<app-truncate [text]="address.bitcoinaddress" [lastChars]="6"></app-truncate>
|
||||
</a>
|
||||
</td>
|
||||
<td class="amount text-right">
|
||||
<app-amount [satoshis]="+address.balance" [noFiat]="true" [forceBtc]="true"></app-amount>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</tbody>
|
||||
<ng-template #skeleton>
|
||||
<tbody *ngIf="widget; else regularRowsSkeleton">
|
||||
<tr *ngFor="let item of skeletonLines">
|
||||
<td class="address text-left widget">
|
||||
<span class="skeleton-loader" style="max-width: 400px"></span>
|
||||
</td>
|
||||
<td class="amount text-right widget">
|
||||
<span class="skeleton-loader" style="max-width: 350px"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<ng-template #regularRowsSkeleton>
|
||||
<tr *ngFor="let item of skeletonLines">
|
||||
<td class="address text-left">
|
||||
<span class="skeleton-loader" style="max-width: 600px"></span>
|
||||
</td>
|
||||
<td class="amount text-right">
|
||||
<span class="skeleton-loader" style="max-width: 400px"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</table>
|
||||
|
||||
<ngb-pagination *ngIf="!widget && federationAddresses$ | async as addresses" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
|
||||
[collectionSize]="addresses.length" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page"
|
||||
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
|
||||
</ngb-pagination>
|
||||
|
||||
<ng-template [ngIf]="!widget">
|
||||
<div class="clearfix"></div>
|
||||
<br>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user