Compare commits

..

1 Commits

Author SHA1 Message Date
junderw
ebf43bd074 Use p-limit to limit concurrent requests 2023-08-18 23:45:03 -07:00
154 changed files with 1459 additions and 5163 deletions

View File

@@ -27,17 +27,8 @@ jobs:
node-version: ${{ matrix.node }}
registry-url: "https://registry.npmjs.org"
- name: Read rust-toolchain file from repository
id: gettoolchain
run: echo "::set-output name=toolchain::$(cat rust-toolchain)"
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}
- name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain
# Latest version available on this commit is 1.71.1
# Commit date is Aug 3, 2023
uses: dtolnay/rust-toolchain@f361669954a8ecfc00a3443f35f9ac8e610ffc06
with:
toolchain: ${{ steps.gettoolchain.outputs.toolchain }}
- name: Install 1.70.x Rust toolchain
uses: dtolnay/rust-toolchain@1.70
- name: Install
if: ${{ matrix.flavor == 'dev'}}

View File

@@ -85,7 +85,7 @@ Install dependencies with `npm` and build the backend:
```
cd backend
npm install --no-install-links # npm@9.4.2 and later can omit the --no-install-links
npm install
npm run build
```

View File

@@ -335,15 +335,13 @@ fn set_relatives(txid: u32, audit_pool: &mut AuditPool) {
let mut total_sigops: u32 = 0;
for ancestor_id in &ancestors {
if let Some(ancestor) = audit_pool
let Some(ancestor) = audit_pool
.get(*ancestor_id as usize)
.expect("audit_pool contains all ancestors")
{
total_fee += ancestor.fee;
total_sigop_adjusted_weight += ancestor.sigop_adjusted_weight;
total_sigop_adjusted_vsize += ancestor.sigop_adjusted_vsize;
total_sigops += ancestor.sigops;
} else { todo!() };
.expect("audit_pool contains all ancestors") else { todo!() };
total_fee += ancestor.fee;
total_sigop_adjusted_weight += ancestor.sigop_adjusted_weight;
total_sigop_adjusted_vsize += ancestor.sigop_adjusted_vsize;
total_sigops += ancestor.sigops;
}
if let Some(Some(tx)) = audit_pool.get_mut(txid as usize) {

View File

@@ -75,9 +75,9 @@ class FailoverRouter {
const results = await Promise.allSettled(this.hosts.map(async (host) => {
if (host.socket) {
return this.pollConnection.get<number>('/blocks/tip/height', { socketPath: host.host, timeout: 5000 });
return this.pollConnection.get<number>('/blocks/tip/height', { socketPath: host.host, timeout: 2000 });
} else {
return this.pollConnection.get<number>(host.host + '/blocks/tip/height', { timeout: 5000 });
return this.pollConnection.get<number>(host.host + '/blocks/tip/height', { timeout: 2000 });
}
}));
const maxHeight = results.reduce((max, result) => Math.max(max, result.status === 'fulfilled' ? result.value?.data || 0 : 0), 0);

View File

@@ -42,12 +42,6 @@ class NodesRoutes {
switch (config.MEMPOOL.NETWORK) {
case 'testnet':
nodesList = [
'0259db43b4e4ac0ff12a805f2d81e521253ba2317f6739bc611d8e2fa156d64256',
'0352b9944b9a52bd2116c91f1ba70c4ef851ac5ba27e1b20f1d92da3ade010dd10',
'03424f5a7601eaa47482cb17100b31a84a04d14fb44b83a57eeceffd8e299878e3',
'032850492ee61a5f7006a2fda6925e4b4ec3782f2b6de2ff0e439ef5a38c3b2470',
'022c80bace98831c44c32fb69755f2b353434e0ee9e7fbda29507f7ef8abea1421',
'02c3559c833e6f99f9ca05fe503e0b4e7524dea9121344edfd3e811101e0c28680',
'032c7c7819276c4f706a04df1a0f1e10a5495994a7be4c1d3d28ca766e5a2b957b',
'025a7e38c2834dd843591a4d23d5f09cdeb77ddca85f673c2d944a14220ff14cf7',
'0395e2731a1673ef21d7a16a727c4fc4d4c35a861c428ce2c819c53d2b81c8bd55',
@@ -70,12 +64,6 @@ class NodesRoutes {
break;
case 'signet':
nodesList = [
'029fe3621fc0c6e08056a14b868f8fb9acca1aa28a129512f6cea0f0d7654d9f92',
'02f60cd7a3a4f1c953dd9554a6ebd51a34f8b10b8124b7fc43a0b381139b55c883',
'03cbbf581774700865eebd1be42d022bc004ba30881274ab304e088a25d70e773d',
'0243348cb3741cfe2d8485fa8375c29c7bc7cbb67577c363cb6987a5e5fd0052cc',
'02cb73e631af44bee600d80f8488a9194c9dc5c7590e575c421a070d1be05bc8e9',
'0306f55ee631aa1e2cd4d9b2bfcbc14404faec5c541cef8b2e6f779061029d09c4',
'03ddab321b760433cbf561b615ef62ac7d318630c5f51d523aaf5395b90b751956',
'033d92c7bfd213ef1b34c90e985fb5dc77f9ec2409d391492484e57a44c4aca1de',
'02ad010dda54253c1eb9efe38b0760657a3b43ecad62198c359c051c9d99d45781',
@@ -98,12 +86,6 @@ class NodesRoutes {
break;
default:
nodesList = [
'02b12b889fe3c943cb05645921040ef13d6d397a2e7a4ad000e28500c505ff26d6',
'0302240ac9d71b39617cbde2764837ec3d6198bd6074b15b75d2ff33108e89d2e1',
'03364a8ace313376e5e4b68c954e287c6388e16df9e9fdbaf0363ecac41105cbf6',
'03229ab4b7f692753e094b93df90530150680f86b535b5183b0cffd75b3df583fc',
'03a696eb7acde991c1be97a58a9daef416659539ae462b897f5e9ae361f990228e',
'0248bf26cf3a63ab8870f34dc0ec9e6c8c6288cdba96ba3f026f34ec0f13ac4055',
'03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61',
'03da9a8623241ccf95f19cd645c6cecd4019ac91570e976eb0a128bebbc4d8a437',
'03ca5340cf85cb2e7cf076e489f785410838de174e40be62723e8a60972ad75144',

View File

@@ -451,7 +451,6 @@ class MempoolBlocks {
private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], accelerations, accelerationPool, saveResults): MempoolBlockWithTransactions[] {
for (const [txid, rate] of rates) {
if (txid in mempool) {
mempool[txid].cpfpDirty = (rate !== mempool[txid].effectiveFeePerVsize);
mempool[txid].effectiveFeePerVsize = rate;
mempool[txid].cpfpChecked = false;
}
@@ -495,9 +494,6 @@ class MempoolBlocks {
}
}
});
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
mempoolTx.cpfpDirty = true;
}
Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true});
}
}
@@ -535,21 +531,12 @@ class MempoolBlocks {
const acceleration = accelerations[txid];
if (isAccelerated[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
if (!mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
}
mempoolTx.acceleration = true;
for (const ancestor of mempoolTx.ancestors || []) {
if (!mempool[ancestor.txid].acceleration) {
mempool[ancestor.txid].cpfpDirty = true;
}
mempool[ancestor.txid].acceleration = true;
isAccelerated[ancestor.txid] = true;
}
} else {
if (mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
}
delete mempoolTx.acceleration;
}

View File

@@ -171,7 +171,6 @@ class StatisticsApi {
private getQueryForDaysAvg(div: number, interval: string) {
return `SELECT
UNIX_TIMESTAMP(added) as added,
CAST(avg(unconfirmed_transactions) as DOUBLE) as unconfirmed_transactions,
CAST(avg(vbytes_per_second) as DOUBLE) as vbytes_per_second,
CAST(avg(vsize_1) as DOUBLE) as vsize_1,
CAST(avg(vsize_2) as DOUBLE) as vsize_2,
@@ -220,7 +219,6 @@ class StatisticsApi {
private getQueryForDays(div: number, interval: string) {
return `SELECT
UNIX_TIMESTAMP(added) as added,
CAST(avg(unconfirmed_transactions) as DOUBLE) as unconfirmed_transactions,
CAST(avg(vbytes_per_second) as DOUBLE) as vbytes_per_second,
vsize_1,
vsize_2,
@@ -403,7 +401,6 @@ class StatisticsApi {
return statistic.map((s) => {
return {
added: s.added,
count: s.unconfirmed_transactions,
vbytes_per_second: s.vbytes_per_second,
mempool_byte_weight: s.mempool_byte_weight,
total_fee: s.total_fee,

View File

@@ -75,7 +75,7 @@ class TransactionUtils {
public async $getMempoolTransactionsExtended(txids: string[], addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<MempoolTransactionExtended[]> {
if (forceCore || config.MEMPOOL.BACKEND !== 'esplora') {
const limiter = pLimit(8); // Run 8 requests at a time
const limiter = pLimit(32); // Run 32 requests at a time
const results = await Promise.allSettled(txids.map(
txid => limiter(() => this.$getMempoolTransactionExtended(txid, addPrevouts, lazyPrevouts, forceCore))
));

View File

@@ -486,7 +486,6 @@ class WebsocketHandler {
// pre-compute address transactions
const addressCache = this.makeAddressCache(newTransactions);
const removedAddressCache = this.makeAddressCache(deletedTransactions);
this.wss.clients.forEach(async (client) => {
if (client.readyState !== WebSocket.OPEN) {
@@ -527,15 +526,11 @@ class WebsocketHandler {
}
if (client['track-address']) {
const newTransactions = Array.from(addressCache[client['track-address']]?.values() || []);
const removedTransactions = Array.from(removedAddressCache[client['track-address']]?.values() || []);
const foundTransactions = Array.from(addressCache[client['track-address']]?.values() || []);
// txs may be missing prevouts in non-esplora backends
// so fetch the full transactions now
const fullTransactions = (config.MEMPOOL.BACKEND !== 'esplora') ? await this.getFullTransactions(newTransactions) : newTransactions;
const fullTransactions = (config.MEMPOOL.BACKEND !== 'esplora') ? await this.getFullTransactions(foundTransactions) : foundTransactions;
if (removedTransactions.length) {
response['address-removed-transactions'] = JSON.stringify(removedTransactions);
}
if (fullTransactions.length) {
response['address-transactions'] = JSON.stringify(fullTransactions);
}
@@ -591,25 +586,13 @@ class WebsocketHandler {
const mempoolTx = newMempool[trackTxid];
if (mempoolTx && mempoolTx.position) {
const positionData = {
response['txPosition'] = JSON.stringify({
txid: trackTxid,
position: {
...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined,
}
};
if (mempoolTx.cpfpDirty) {
positionData['cpfp'] = {
ancestors: mempoolTx.ancestors,
bestDescendant: mempoolTx.bestDescendant || null,
descendants: mempoolTx.descendants || null,
effectiveFeePerVsize: mempoolTx.effectiveFeePerVsize || null,
sigops: mempoolTx.sigops,
adjustedVsize: mempoolTx.adjustedVsize,
acceleration: mempoolTx.acceleration
};
}
response['txPosition'] = JSON.stringify(positionData);
});
}
}

View File

@@ -104,7 +104,6 @@ export interface MempoolTransactionExtended extends TransactionExtended {
adjustedFeePerVsize: number;
inputs?: number[];
lastBoosted?: number;
cpfpDirty?: boolean;
}
export interface AuditTransaction {

View File

@@ -1,3 +0,0 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file with sha256 hash c80c5ee4c71c5a76a1f6cd35339bd0c45b25b491933ea7b02a66470e9f43a6fd.
Signed: TheBlueMatt

View File

@@ -1,3 +0,0 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of September 12, 2023.
Signed: orange surf

File diff suppressed because it is too large Load Diff

View File

@@ -61,18 +61,18 @@
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
},
"dependencies": {
"@angular-devkit/build-angular": "^16.2.0",
"@angular/animations": "^16.2.2",
"@angular/cli": "^16.2.0",
"@angular/common": "^16.2.2",
"@angular/compiler": "^16.2.2",
"@angular/core": "^16.2.2",
"@angular/forms": "^16.2.2",
"@angular/localize": "^16.2.2",
"@angular/platform-browser": "^16.2.2",
"@angular/platform-browser-dynamic": "^16.2.2",
"@angular/platform-server": "^16.2.2",
"@angular/router": "^16.2.2",
"@angular-devkit/build-angular": "^16.1.4",
"@angular/animations": "^16.1.5",
"@angular/cli": "^16.1.4",
"@angular/common": "^16.1.5",
"@angular/compiler": "^16.1.5",
"@angular/core": "^16.1.5",
"@angular/forms": "^16.1.5",
"@angular/localize": "^16.1.5",
"@angular/platform-browser": "^16.1.5",
"@angular/platform-browser-dynamic": "^16.1.5",
"@angular/platform-server": "^16.1.5",
"@angular/router": "^16.1.5",
"@fortawesome/angular-fontawesome": "~0.13.0",
"@fortawesome/fontawesome-common-types": "~6.4.0",
"@fortawesome/fontawesome-svg-core": "~6.4.0",

View File

@@ -112,14 +112,6 @@ PROXY_CONFIG.push(...[
"^/testnet": ""
},
},
{
context: ['/api/v1/services/**'],
target: `http://localhost:9000`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
},
{
context: ['/api/v1/**'],
target: `http://127.0.0.1:8999`,

View File

@@ -112,14 +112,6 @@ PROXY_CONFIG.push(...[
"^/testnet": ""
},
},
{
context: ['/api/v1/services/**'],
target: `http://localhost:9000`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
},
{
context: ['/api/v1/**'],
target: `http://localhost:8999`,

View File

@@ -95,14 +95,6 @@ if (configContent && configContent.BASE_MODULE === 'bisq') {
}
PROXY_CONFIG.push(...[
{
context: ['/api/v1/services/**'],
target: `http://localhost:9000`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
},
{
context: ['/api/v1/**'],
target: `http://localhost:8999`,

View File

@@ -4,7 +4,6 @@ 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 { ClockComponent } from './components/clock/clock.component';
import { AddressComponent } from './components/address/address.component';
import { MasterPageComponent } from './components/master-page/master-page.component';
@@ -374,10 +373,6 @@ let routes: Routes = [
path: 'clock/:mode/:index',
component: ClockComponent,
},
{
path: 'view/block/:id',
component: BlockViewComponent,
},
{
path: 'status',
data: { networks: ['bitcoin', 'liquid'] },

View File

@@ -41,7 +41,6 @@ export class BisqAddressComponent implements OnInit, OnDestroy {
document.body.scrollTo(0, 0);
this.addressString = params.get('id') || '';
this.seoService.setTitle($localize`:@@bisq-address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
this.seoService.setDescription($localize`:@@meta.description.bisq.address:See current balance, pending transactions, and history of confirmed transactions for BSQ address ${this.addressString}:INTERPOLATION:.`);
return this.bisqApiService.getAddress$(this.addressString)
.pipe(

View File

@@ -69,7 +69,6 @@ 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)));
}),
@@ -89,7 +88,6 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
this.isLoading = false;
this.blockHeight = block.height;
this.seoService.setTitle($localize`:@@bisq-block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.hash}:BLOCK_HASH:`);
this.seoService.setDescription($localize`:@@meta.description.bisq.block:See all BSQ transactions in Bitcoin block ${block.height}:BLOCK_HEIGHT: (block hash ${block.hash}:BLOCK_HASH:).`);
this.block = block;
});
}

View File

@@ -36,7 +36,6 @@ export class BisqBlocksComponent implements OnInit {
ngOnInit(): void {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`);
this.seoService.setDescription($localize`:@@meta.description.bisq.blocks:See a list of recent Bitcoin blocks with BSQ transactions, total BSQ sent per block, and more.`);
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
this.loadingItems = Array(this.itemsPerPage);
if (document.body.clientWidth < 670) {

View File

@@ -29,8 +29,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.setTitle(`Markets`);
this.websocketService.want(['blocks']);
this.volumes$ = this.bisqApiService.getAllVolumesDay$()

View File

@@ -34,7 +34,6 @@ export class BisqMainDashboardComponent implements OnInit {
ngOnInit(): void {
this.seoService.resetTitle();
this.seoService.resetDescription();
this.websocketService.want(['blocks']);
this.usdPrice$ = this.stateService.conversions$.asObservable().pipe(

View File

@@ -48,8 +48,7 @@ export class BisqMarketComponent implements OnInit, OnDestroy {
map(([markets, routeParams]) => {
const pair = routeParams.get('pair');
const pairUpperCase = pair.replace('_', '/').toUpperCase();
this.seoService.setTitle($localize`:@@meta.title.bisq.market:Bisq market: ${pairUpperCase}`);
this.seoService.setDescription($localize`:@@meta.description.bisq.market:See price history, current buy/sell offers, and latest trades for the ${pairUpperCase} market on Bisq.`);
this.seoService.setTitle(`Bisq market: ${pairUpperCase}`);
return {
pair: pairUpperCase,

View File

@@ -26,7 +26,6 @@ export class BisqStatsComponent implements OnInit {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@2a30a4cdb123a03facc5ab8c5b3e6d8b8dbbc3d4:BSQ statistics`);
this.seoService.setDescription($localize`:@@meta.description.bisq.stats:See high-level stats on the BSQ economy: supply metrics, number of addresses, BSQ price, market cap, and more.`);
this.stateService.bsqPrice$
.subscribe((bsqPrice) => {
this.price = bsqPrice;

View File

@@ -48,7 +48,6 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
document.body.scrollTo(0, 0);
this.txId = params.get('id') || '';
this.seoService.setTitle($localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`);
this.seoService.setDescription($localize`:@@meta.description.bisq.transaction:See inputs, outputs, transaction type, burnt amount, and more for transaction with txid ${this.txId}:INTERPOLATION:.`);
if (history.state.data) {
return of(history.state.data);
}

View File

@@ -79,7 +79,6 @@ export class BisqTransactionsComponent implements OnInit, OnDestroy {
ngOnInit(): void {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@add4cd82e3e38a3110fe67b3c7df56e9602644ee:Transactions`);
this.seoService.setDescription($localize`:@@meta.description.bisq.transactions:See recent BSQ transactions: amount, txid, associated Bitcoin block, transaction type, and more.`);
this.radioGroupForm = this.formBuilder.group({
txTypes: [this.txTypesDefaultChecked],

View File

@@ -4,13 +4,12 @@
<span style="margin-left: auto; margin-right: -20px; margin-bottom: -20px">&reg;</span>
<img class="logo" src="/resources/mempool-logo-bigger.png" />
<div class="version">
<span>v{{ packetJsonVersion }} [<a href="https://github.com/mempool/mempool/commit/{{ frontendGitCommitHash }}">{{ frontendGitCommitHash }}</a>]</span>
<span *ngIf="stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE">[{{ stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE }}]</span>
v{{ packetJsonVersion }} [<a href="https://github.com/mempool/mempool/commit/{{ frontendGitCommitHash }}">{{ frontendGitCommitHash }}</a>]
</div>
</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'"> &reg;</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'"> &trade;</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>
@@ -243,7 +242,7 @@
<img class="image" src="/resources/profile/ronindojo.png" />
<span>RoninDojo</span>
</a>
<a href="https://github.com/runcitadel" target="_blank" title="Citadel">
<a href="https://github.com/runcitadel/core" target="_blank" title="Citadel">
<img class="image" src="/resources/profile/runcitadel.svg" />
<span>Citadel</span>
</a>

View File

@@ -22,7 +22,6 @@
.intro {
margin: 25px auto 30px;
margin-top: 25px;
width: 250px;
display: flex;
flex-direction: column;

View File

@@ -43,7 +43,6 @@ 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.websocketService.want(['blocks']);
this.profiles$ = this.apiService.getAboutPageProfiles$().pipe(

View File

@@ -1,21 +0,0 @@
<div class="fee-graph" *ngIf="tx && estimate">
<div class="column">
<ng-container *ngFor="let bar of bars">
<div class="bar {{ bar.class }}" [class.active]="bar.active" [style]="bar.style" (click)="onClick($event, bar);">
<div class="fill"></div>
<div class="line">
<p class="fee-rate">
<span class="label">{{ bar.label }}</span>
<span class="rate">
<app-fee-rate [fee]="bar.rate"></app-fee-rate>
</span>
</p>
</div>
<div class="spacer"></div>
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }} {{ bar.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
<div class="spacer"></div>
<div class="spacer"></div>
</div>
</ng-container>
</div>
</div>

View File

@@ -1,157 +0,0 @@
.fee-graph {
height: 100%;
min-width: 120px;
width: 120px;
max-height: 90vh;
margin-left: 4em;
margin-right: 1.5em;
padding-bottom: 63px;
.column {
width: 100%;
height: 100%;
position: relative;
background: #181b2d;
.bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.fill {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
opacity: 0.75;
pointer-events: none;
}
.fee {
font-size: 0.9em;
opacity: 0;
pointer-events: none;
}
.spacer {
width: 100%;
height: 1px;
flex-grow: 1;
pointer-events: none;
}
.line {
position: absolute;
right: 0;
top: 0;
left: -4.5em;
border-top: dashed white 1.5px;
.fee-rate {
width: 100%;
position: absolute;
left: 0;
right: 0.2em;
font-size: 0.8em;
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
margin: 0;
.label {
margin-right: .2em;
}
.rate .symbol {
color: white;
}
}
}
&.tx {
.fill {
background: #3bcc49;
}
.line {
.fee-rate {
top: 0;
}
}
.fee {
position: absolute;
opacity: 1;
z-index: 11;
}
}
&.target {
.fill {
background: #653b9c;
}
.fee {
position: absolute;
opacity: 1;
z-index: 11;
}
.line .fee-rate {
bottom: 2px;
}
}
&.max {
cursor: pointer;
.line .fee-rate {
.label {
opacity: 0;
}
bottom: 2px;
}
&.active, &:hover {
.fill {
background: #105fb0;
}
.line {
.fee-rate .label {
opacity: 1;
}
}
}
}
&:hover {
.fill {
z-index: 10;
}
.line {
z-index: 11;
}
.fee {
opacity: 1;
z-index: 12;
}
}
}
&:hover > .bar:not(:hover) {
&.target, &.max {
.fee {
opacity: 0;
}
.line .fee-rate .label {
opacity: 0;
}
}
&.max {
.fill {
background: none;
}
}
}
}
}

View File

@@ -1,96 +0,0 @@
import { Component, OnInit, Input, Output, OnChanges, EventEmitter, HostListener, Inject, LOCALE_ID } from '@angular/core';
import { StateService } from '../../services/state.service';
import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface';
import { Router } from '@angular/router';
import { ReplaySubject, merge, Subscription, of } from 'rxjs';
import { tap, switchMap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';
import { AccelerationEstimate, RateOption } from './accelerate-preview.component';
interface GraphBar {
rate: number;
style: any;
class: 'tx' | 'target' | 'max';
label: string;
active?: boolean;
rateIndex?: number;
fee?: number;
}
@Component({
selector: 'app-accelerate-fee-graph',
templateUrl: './accelerate-fee-graph.component.html',
styleUrls: ['./accelerate-fee-graph.component.scss'],
})
export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
@Input() tx: Transaction;
@Input() estimate: AccelerationEstimate;
@Input() maxRateOptions: RateOption[] = [];
@Input() maxRateIndex: number = 0;
@Output() setUserBid = new EventEmitter<{ fee: number, index: number }>();
bars: GraphBar[] = [];
tooltipPosition = { x: 0, y: 0 };
ngOnInit(): void {
this.initGraph();
}
ngOnChanges(): void {
this.initGraph();
}
initGraph(): void {
if (!this.tx || !this.estimate) {
return;
}
const maxRate = Math.max(...this.maxRateOptions.map(option => option.rate));
const baseRate = this.estimate.txSummary.effectiveFee / this.estimate.txSummary.effectiveVsize;
const baseHeight = baseRate / maxRate;
const bars: GraphBar[] = this.maxRateOptions.slice().reverse().map(option => {
return {
rate: option.rate,
style: this.getStyle(option.rate, maxRate, baseHeight),
class: 'max',
label: 'maximum',
active: option.index === this.maxRateIndex,
rateIndex: option.index,
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
});
bars.push({
rate: baseRate,
style: this.getStyle(baseRate, maxRate, 0),
class: 'tx',
label: '',
fee: this.estimate.txSummary.effectiveFee,
});
this.bars = bars;
}
getStyle(rate, maxRate, base) {
const top = (rate / maxRate);
return {
height: `${(top - base) * 100}%`,
bottom: base ? `${base * 100}%` : '0',
}
}
onClick(event, bar): void {
if (bar.rateIndex != null) {
this.setUserBid.emit({ fee: bar.fee, index: bar.rateIndex });
}
}
@HostListener('pointermove', ['$event'])
onPointerMove(event) {
this.tooltipPosition = { x: event.offsetX, y: event.offsetY };
}
}

View File

@@ -1,262 +0,0 @@
<div class="row" *ngIf="showSuccess">
<div class="col" id="successAlert">
<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>.
</div>
</div>
</div>
<div class="row" *ngIf="error">
<div class="col" id="mempoolError">
<app-mempool-error [error]="error"></app-mempool-error>
</div>
</div>
<div class="accelerate-cols">
<ng-container *ngIf="!isMobile">
<app-accelerate-fee-graph
[tx]="tx"
[estimate]="estimate"
[maxRateOptions]="maxRateOptions"
[maxRateIndex]="selectFeeRateIndex"
(setUserBid)="setUserBid($event)"
></app-accelerate-fee-graph>
</ng-container>
<ng-container *ngIf="estimate">
<div [class]="{estimateDisabled: error}">
<h5>Your transaction</h5>
<div class="row">
<div class="col">
<small *ngIf="hasAncestors" class="form-text text-muted mb-2">
Plus {{ estimate.txSummary.ancestorCount - 1 }} unconfirmed ancestor{{ estimate.txSummary.ancestorCount > 2 ? 's' : ''}}.
</small>
<table class="table table-borderless table-border table-dark table-accelerator">
<tbody>
<tr class="group-first">
<td class="item">
Virtual size
</td>
<td class="units" [innerHTML]="'&lrm;' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td>
</tr>
<tr class="info">
<td class="info">
<i><small>Size in vbytes of this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i>
</td>
</tr>
<tr>
<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>
</tr>
<tr class="info group-last">
<td class="info">
<i><small>Fees already paid by this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<br>
<h5>How much more are you willing to pay?</h5>
<div class="row">
<div class="col">
<small class="form-text text-muted mb-2">
Choose the maximum extra transaction fee you're willing to pay to get into the next block.<br>
If the estimated next block rate rises beyond this limit, we will automatically cancel your acceleration request.
</small>
<div class="form-group">
<div class="fee-card">
<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>
</button>
</ng-container>
</div>
</div>
</div>
</div>
</div>
<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'">
<tr class="group-first">
<td class="item">
Next block market rate
</td>
<td class="amt" style="font-size: 20px">
{{ estimate.targetFeeRate | 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>Estimated extra fee required</small></i>
</td>
<td class="amt">
{{ 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>
</td>
</tr>
</ng-container>
<!-- MEMPOOL BASE FEE -->
<tr>
<td class="item">
Mempool Accelerator™ fees
</td>
</tr>
<tr class="info">
<td class="info">
<i><small>mempool.space 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>
</td>
</tr>
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
<td class="info">
<i><small>Transaction vsize fee</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>
</td>
</tr>
<!-- NEXT BLOCK ESTIMATE -->
<ng-container *ngIf="showTable === 'estimated'">
<tr class="group-first">
<td class="item">
<b style="background-color: #5E35B1" class="p-1 pl-0">Estimated acceleration cost</b>
</td>
<td class="amt">
<span style="background-color: #5E35B1" class="p-1 pl-0">
{{ estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee | number }}
</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>
</td>
</tr>
<tr class="info group-last">
<td class="info">
<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'">
<tr class="group-first">
<td class="item">
<b style="background-color: #105fb0;" class="p-1 pl-0">Maximum acceleration cost</b>
</td>
<td class="amt">
<span style="background-color: #105fb0" class="p-1 pl-0">
{{ maxCost | number }}
</span>
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat">
<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">
<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">
<tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item">
Available balance
</td>
<td class="amt">
{{ estimate.userBalance | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat">
<app-fiat [value]="estimate.userBalance" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
</span>
</td>
</tr>
</ng-container>
</tbody>
</table>
</div>
</div>
<div class="row mb-3" *ngIf="isLoggedIn()">
<div class="col">
<div class="d-flex justify-content-end">
<button class="btn btn-sm btn-primary btn-success" style="width: 150px" (click)="accelerate()">Accelerate</button>
</div>
</div>
</div>
</div>
</ng-container>
</div>

View File

@@ -1,88 +0,0 @@
.fee-card {
padding: 15px;
background-color: #1d1f31;
.feerate {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.fee {
font-size: 1.2em;
}
.rate {
font-size: 0.9em;
.symbol {
color: white;
}
}
}
}
.btn-border {
border: solid 1px black;
background-color: #0c4a87;
}
.feerate.active {
background-color: #105fb0 !important;
opacity: 1;
border: 1px solid white !important;
}
.estimateDisabled {
opacity: 0.5;
pointer-events: none;
}
.table-toggle {
width: 100%;
margin-top: 0.5em;
}
.table-accelerator {
tr {
text-wrap: wrap;
td {
padding-top: 0;
padding-bottom: 0;
vertical-align: baseline;
}
&.group-first {
td {
padding-top: 0.75rem;
}
}
&.group-last {
td {
padding-bottom: 0.75rem;
}
}
}
td {
&:first-child {
width: 100vw;
}
&.info {
color: #6c757d;
}
&.amt {
text-align: right;
padding-right: 0.2em;
}
&.units {
padding-left: 0.2em;
white-space: nowrap;
}
}
}
.accelerate-cols {
display: flex;
flex-direction: row;
align-items: stretch;
margin-top: 1em;
}

View File

@@ -1,205 +0,0 @@
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener } 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';
export type AccelerationEstimate = {
txSummary: TxSummary;
nextBlockFee: number;
targetFeeRate: number;
userBalance: number;
enoughBalance: boolean;
cost: number;
mempoolBaseFee: number;
vsizeFee: number;
}
export type TxSummary = {
txid: string; // txid of the current transaction
effectiveVsize: number; // Total vsize of the dependency tree
effectiveFee: number; // Total fee of the dependency tree in sats
ancestorCount: number; // Number of ancestors
}
export interface RateOption {
fee: number;
rate: number;
index: number;
}
export const MIN_BID_RATIO = 1;
export const DEFAULT_BID_RATIO = 2;
export const MAX_BID_RATIO = 4;
@Component({
selector: 'app-accelerate-preview',
templateUrl: 'accelerate-preview.component.html',
styleUrls: ['accelerate-preview.component.scss']
})
export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges {
@Input() tx: Transaction | undefined;
@Input() scrollEvent: boolean;
math = Math;
error = '';
showSuccess = false;
estimateSubscription: Subscription;
accelerationSubscription: Subscription;
estimate: any;
hasAncestors: boolean = false;
minExtraCost = 0;
minBidAllowed = 0;
maxBidAllowed = 0;
defaultBid = 0;
maxCost = 0;
userBid = 0;
selectFeeRateIndex = 1;
showTable: 'estimated' | 'maximum' = 'maximum';
isMobile: boolean = window.innerWidth <= 767.98;
maxRateOptions: RateOption[] = [];
constructor(
private apiService: ApiService,
private storageService: StorageService
) { }
ngOnDestroy(): void {
if (this.estimateSubscription) {
this.estimateSubscription.unsubscribe();
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.scrollEvent) {
this.scrollToPreview('acceleratePreviewAnchor', 'center');
}
}
ngOnInit() {
this.estimateSubscription = this.apiService.estimate$(this.tx.txid).pipe(
tap((response) => {
if (response.status === 204) {
this.estimate = undefined;
this.error = `cannot_accelerate_tx`;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
this.estimateSubscription.unsubscribe();
} else {
this.estimate = response.body;
if (!this.estimate) {
this.error = `cannot_accelerate_tx`;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
this.estimateSubscription.unsubscribe();
}
if (this.estimate.userBalance <= 0) {
if (this.isLoggedIn()) {
this.error = `not_enough_balance`;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
}
}
this.hasAncestors = this.estimate.txSummary.ancestorCount > 1;
// Make min extra fee at least 50% of the current tx fee
this.minExtraCost = nextRoundNumber(Math.max(this.estimate.cost * 2, this.estimate.txSummary.effectiveFee));
this.maxRateOptions = [1, 2, 4].map((multiplier, index) => {
return {
fee: this.minExtraCost * multiplier,
rate: (this.estimate.txSummary.effectiveFee + (this.minExtraCost * multiplier)) / this.estimate.txSummary.effectiveVsize,
index,
};
});
this.minBidAllowed = this.minExtraCost * MIN_BID_RATIO;
this.defaultBid = this.minExtraCost * DEFAULT_BID_RATIO;
this.maxBidAllowed = this.minExtraCost * MAX_BID_RATIO;
this.userBid = this.defaultBid;
if (this.userBid < this.minBidAllowed) {
this.userBid = this.minBidAllowed;
} else if (this.userBid > this.maxBidAllowed) {
this.userBid = this.maxBidAllowed;
}
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
if (!this.error) {
this.scrollToPreview('acceleratePreviewAnchor', 'center');
}
}
}),
catchError((response) => {
this.estimate = undefined;
this.error = response.error;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
this.estimateSubscription.unsubscribe();
return of(null);
})
).subscribe();
}
/**
* User changed his bid
*/
setUserBid({ fee, index }: { fee: number, index: number}) {
if (this.estimate) {
this.selectFeeRateIndex = index;
this.userBid = Math.max(0, fee);
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
}
}
/**
* Scroll to element id with or without setTimeout
*/
scrollToPreviewWithTimeout(id: string, position: ScrollLogicalPosition) {
setTimeout(() => {
this.scrollToPreview(id, position);
}, 100);
}
scrollToPreview(id: string, position: ScrollLogicalPosition) {
const acceleratePreviewAnchor = document.getElementById(id);
if (acceleratePreviewAnchor) {
acceleratePreviewAnchor.scrollIntoView({
behavior: 'smooth',
inline: position,
block: position,
});
}
}
/**
* Send acceleration request
*/
accelerate() {
if (this.accelerationSubscription) {
this.accelerationSubscription.unsubscribe();
}
this.accelerationSubscription = this.apiService.accelerate$(
this.tx.txid,
this.userBid
).subscribe({
next: () => {
this.showSuccess = true;
this.scrollToPreviewWithTimeout('successAlert', 'center');
this.estimateSubscription.unsubscribe();
},
error: (response) => {
this.error = response.error;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
}
});
}
isLoggedIn() {
const auth = this.storageService.getAuth();
return auth !== null;
}
@HostListener('window:resize', ['$event'])
onResize(): void {
this.isMobile = window.innerWidth <= 767.98;
}
}

View File

@@ -9,7 +9,6 @@ import { AudioService } from '../../services/audio.service';
import { ApiService } from '../../services/api.service';
import { of, merge, Subscription, Observable } from 'rxjs';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { AddressInformation } from '../../interfaces/node-api.interface';
@Component({
@@ -69,7 +68,6 @@ 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:.`);
return (this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
? this.electrsApiService.getPubKeyAddress$(this.addressString)

View File

@@ -9,7 +9,6 @@ import { AudioService } from '../../services/audio.service';
import { ApiService } from '../../services/api.service';
import { of, merge, Subscription, Observable } from 'rxjs';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { AddressInformation } from '../../interfaces/node-api.interface';
@Component({
@@ -77,7 +76,6 @@ export class AddressComponent 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:.`);
return merge(
of(true),
@@ -174,11 +172,6 @@ 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);
@@ -227,30 +220,6 @@ 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) {
return;

View File

@@ -40,7 +40,6 @@ export class AssetsNavComponent implements OnInit {
ngOnInit(): void {
this.seoService.setTitle($localize`:@@ee8f8008bae6ce3a49840c4e1d39b4af23d4c263:Assets`);
this.seoService.setDescription($localize`:@@meta.description.liquid.assets:Explore all the assets issued on the Liquid network like L-BTC, L-CAD, USDT, and more.`);
this.typeaheadSearchFn = this.typeaheadSearch;
this.searchForm = this.formBuilder.group({

View File

@@ -64,7 +64,6 @@ export class BlockFeeRatesGraphComponent implements OnInit {
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');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);

View File

@@ -65,7 +65,6 @@ export class BlockFeesGraphComponent implements OnInit {
ngOnInit(): void {
this.seoService.setTitle($localize`:@@6c453b11fd7bd159ae30bc381f367bc736d86909:Block Fees`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-fees:See the average mining fees earned per Bitcoin block visualized in BTC and USD over time.`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('1m');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
@@ -193,7 +192,7 @@ export class BlockFeesGraphComponent implements OnInit {
{
name: 'Fees ' + this.currency,
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
textStyle: {
color: 'white',
},
icon: 'roundRect',

View File

@@ -61,7 +61,6 @@ export class BlockHealthGraphComponent implements OnInit {
ngOnInit(): void {
this.seoService.setTitle($localize`:@@d7d5fcf50179ad70c938491c517efb82de2c8146:Block Health`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-health:See Bitcoin block health visualized over time. Block health is a measure of how many expected transactions were included in an actual mined block. Expected transactions are determined using Mempool's re-implementation of Bitcoin Core's transaction selection algorithm.`);
this.miningWindowPreference = '24h';//this.miningService.getDefaultTimespan('24h');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);

View File

@@ -70,11 +70,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
this.gl = this.canvas.nativeElement.getContext('webgl');
this.initCanvas();
if (this.gl) {
this.initCanvas();
this.resizeCanvas();
}
this.resizeCanvas();
}
ngOnChanges(changes): void {
@@ -197,16 +195,10 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
cancelAnimationFrame(this.animationFrameRequest);
this.animationFrameRequest = null;
this.running = false;
this.gl = null;
}
handleContextRestored(event): void {
if (this.canvas?.nativeElement) {
this.gl = this.canvas.nativeElement.getContext('webgl');
if (this.gl) {
this.initCanvas();
}
}
this.initCanvas();
}
@HostListener('window:resize', ['$event'])
@@ -232,9 +224,6 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
compileShader(src, type): WebGLShader {
if (!this.gl) {
return;
}
const shader = this.gl.createShader(type);
this.gl.shaderSource(shader, src);
@@ -248,9 +237,6 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
buildShaderProgram(shaderInfo): WebGLProgram {
if (!this.gl) {
return;
}
const program = this.gl.createProgram();
shaderInfo.forEach((desc) => {
@@ -287,7 +273,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
now = performance.now();
}
// skip re-render if there's no change to the scene
if (this.scene && this.gl) {
if (this.scene) {
/* SET UP SHADER UNIFORMS */
// screen dimensions
this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight);

View File

@@ -63,7 +63,6 @@ export class BlockRewardsGraphComponent implements OnInit {
ngOnInit(): void {
this.seoService.setTitle($localize`:@@8ba8fe810458280a83df7fdf4c614dfc1a826445:Block Rewards`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-rewards:See Bitcoin block rewards in BTC and USD visualized over time. Block rewards are the total funds miners earn from the block subsidy and fees.`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('3m');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
@@ -192,7 +191,7 @@ export class BlockRewardsGraphComponent implements OnInit {
{
name: 'Rewards ' + this.currency,
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
textStyle: {
color: 'white',
},
icon: 'roundRect',

View File

@@ -60,7 +60,6 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
let firstRun = true;
this.seoService.setTitle($localize`:@@56fa1cd221491b6478998679cba2dc8d55ba330d:Block Sizes and Weights`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-sizes:See Bitcoin block sizes (MB) and block weights (weight units) visualized over time.`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);

View File

@@ -1,13 +0,0 @@
<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"
></app-block-overview-graph>
</div>
</div>

View File

@@ -1,22 +0,0 @@
.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;
}
}

View File

@@ -1,176 +0,0 @@
import { Component, OnInit, OnDestroy, ViewChild, HostListener } from '@angular/core';
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 { StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
import { OpenGraphService } from '../../services/opengraph.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';
function bestFitResolution(min, max, n) {
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 electrsApiService: ElectrsApiService,
public stateService: StateService,
private seoService: SeoService,
private openGraphService: OpenGraphService,
private apiService: ApiService
) { }
ngOnInit() {
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);
}
@HostListener('window:resize', ['$event'])
onResize(): void {
if (this.autofit) {
this.resolution = bestFitResolution(64, 96, Math.min(window.innerWidth, window.innerHeight));
console.log('resized, new resolution ', this.resolution, window.innerWidth, window.innerHeight);
// if (this.blockGraph && this.strippedTransactions) {
// this.blockGraph.destroy();
// this.blockGraph.setup(this.strippedTransactions);
// }
}
}
ngOnDestroy() {
if (this.overviewSubscription) {
this.overviewSubscription.unsubscribe();
}
if (this.networkChangedSubscription) {
this.networkChangedSubscription.unsubscribe();
}
if (this.queryParamsSubscription) {
this.queryParamsSubscription.unsubscribe();
}
}
}

View File

@@ -8,7 +8,6 @@ import { SeoService } from '../../services/seo.service';
import { OpenGraphService } from '../../services/opengraph.service';
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
@Component({
@@ -98,11 +97,6 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
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.setBlockSubsidy();
if (block?.extras?.reward !== undefined) {

View File

@@ -13,7 +13,6 @@ import { BlockAudit, BlockExtended, TransactionStripped } from '../../interfaces
import { ApiService } from '../../services/api.service';
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
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';
@@ -166,6 +165,7 @@ 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;
@@ -175,7 +175,6 @@ 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,7 +201,6 @@ 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;
@@ -263,11 +261,6 @@ export class BlockComponent implements OnInit, OnDestroy {
this.setNextAndPreviousBlockLink();
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.setBlockSubsidy();
if (block?.extras?.reward !== undefined) {
@@ -332,7 +325,7 @@ export class BlockComponent implements OnInit, OnDestroy {
]);
})
)
.subscribe(([transactions, blockAudit]) => {
.subscribe(([transactions, blockAudit]) => {
if (transactions) {
this.strippedTransactions = transactions;
} else {
@@ -687,7 +680,7 @@ export class BlockComponent implements OnInit, OnDestroy {
this.setAuditAvailable(false);
}
}
isAuditAvailableFromBlockHeight(blockHeight: number): boolean {
if (!this.auditSupported) {
return false;
@@ -736,4 +729,4 @@ export class BlockComponent implements OnInit, OnDestroy {
this.block.canonical = block.id;
}
}
}
}

View File

@@ -1,5 +1,5 @@
<div class="text-center" class="blockchain-wrapper" [class.time-ltr]="timeLtr" [class.ltr-transition]="ltrTransitionEnabled" #container>
<div #positionContainer class="position-container" [ngClass]="network ? network : ''" [style]="positionStyle">
<div class="position-container" [ngClass]="network ? network : ''" [style.--divider-offset]="dividerOffset + 'px'" [style.--mempool-offset]="mempoolOffset + 'px'">
<span>
<div class="blocks-wrapper">
<div class="scroll-spacer" *ngIf="minScrollWidth" [style.left]="minScrollWidth + 'px'"></div>

View File

@@ -26,7 +26,15 @@
position: absolute;
left: 0;
top: 75px;
transform: translateX(1280px);
--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)));
}
}
.black-background {

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, Output, EventEmitter, ChangeDetectorRef, OnChanges, SimpleChanges } from '@angular/core';
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, Output, EventEmitter, HostListener, ChangeDetectorRef } from '@angular/core';
import { firstValueFrom, Subscription } from 'rxjs';
import { StateService } from '../../services/state.service';
@@ -8,13 +8,12 @@ import { StateService } from '../../services/state.service';
styleUrls: ['./blockchain.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
export class BlockchainComponent implements OnInit, OnDestroy {
@Input() pages: any[] = [];
@Input() pageIndex: number;
@Input() blocksPerPage: number = 8;
@Input() minScrollWidth: number = 0;
@Input() scrollableMempool: boolean = false;
@Input() containerWidth: number;
@Output() mempoolOffsetChange: EventEmitter<number> = new EventEmitter();
@@ -27,11 +26,8 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
loadingTip: boolean = true;
connected: boolean = true;
dividerOffset: number | null = null;
mempoolOffset: number | null = null;
positionStyle = {
transform: "translateX(1280px)",
};
dividerOffset: number = 0;
mempoolOffset: number = 0;
constructor(
public stateService: StateService,
@@ -43,7 +39,6 @@ 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);
@@ -67,68 +62,44 @@ 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 || 0));
this.updateStyle();
this.cd.markForCheck();
this.mempoolOffsetChange.emit(this.mempoolOffset);
}, 1000);
}, 0);
this.cd.markForCheck();
}
onMempoolWidthChange(width): void {
if (this.flipping) {
return;
}
this.mempoolOffset = Math.max(0, width - (this.dividerOffset || 0));
this.updateStyle();
this.mempoolOffset = Math.max(0, width - this.dividerOffset);
this.cd.markForCheck();
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();
}
}
@HostListener('window:resize', ['$event'])
onResize(): void {
const width = this.containerWidth || window.innerWidth;
if (width >= 768) {
if (window.innerWidth >= 768) {
if (this.stateService.isLiquid()) {
this.dividerOffset = 420;
} else {
this.dividerOffset = width * 0.5;
this.dividerOffset = window.innerWidth * 0.5;
}
} else {
if (this.stateService.isLiquid()) {
this.dividerOffset = width * 0.5;
this.dividerOffset = window.innerWidth * 0.5;
} else {
this.dividerOffset = width * 0.95;
this.dividerOffset = window.innerWidth * 0.95;
}
}
this.updateStyle();
this.cd.markForCheck();
}
}

View File

@@ -5,8 +5,6 @@ import { BlockExtended } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
@Component({
selector: 'app-blocks-list',
@@ -37,7 +35,6 @@ export class BlocksList implements OnInit {
private websocketService: WebsocketService,
public stateService: StateService,
private cd: ChangeDetectorRef,
private seoService: SeoService,
) {
}
@@ -53,14 +50,6 @@ 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.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 {
this.seoService.setDescription($localize`:@@meta.description.bitcoin.blocks:See the most recent Bitcoin${seoDescriptionNetwork(this.stateService.network)} blocks along with basic stats such as block height, block reward, block size, and more.`);
}
this.blocks$ = combineLatest([
this.fromHeightSubject.pipe(
switchMap((fromBlockHeight) => {
@@ -140,4 +129,4 @@ export class BlocksList implements OnInit {
isEllipsisActive(e): boolean {
return (e.offsetWidth < e.scrollWidth);
}
}
}

View File

@@ -194,7 +194,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 +202,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();
}

View File

@@ -1,5 +1,5 @@
<div [formGroup]="fiatForm" class="text-small text-center">
<select formControlName="fiat" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 85px;" (change)="changeFiat()">
<option *ngFor="let currency of currencies" [value]="currency[1].code">{{ currency[1].code }}</option>
<select formControlName="fiat" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 150px;" (change)="changeFiat()">
<option *ngFor="let currency of currencies" [value]="currency[1].code">{{ currency[1].name + " (" + currency[1].code + ")" }}</option>
</select>
</div>

View File

@@ -12,7 +12,6 @@ import { MiningService } from '../../services/mining.service';
import { download } from '../../shared/graphs.utils';
import { ActivatedRoute } from '@angular/router';
import { StateService } from '../../services/state.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
@Component({
selector: 'app-hashrate-chart',
@@ -72,7 +71,6 @@ export class HashrateChartComponent implements OnInit {
this.miningWindowPreference = '1y';
} else {
this.seoService.setTitle($localize`:@@3510fc6daa1d975f331e3a717bdf1a34efa06dff:Hashrate & Difficulty`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.hashrate:See hashrate and difficulty for the Bitcoin${seoDescriptionNetwork(this.network)} network visualized over time.`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('3m');
}
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
@@ -258,7 +256,7 @@ export class HashrateChartComponent implements OnInit {
let difficultyPowerOfTen = hashratePowerOfTen;
let difficulty = tick.data[1];
if (difficulty === null) {
difficultyString = `${tick.marker} ${tick.seriesName}: No data<br>`;
difficultyString = `${tick.marker} ${tick.seriesName}: No data<br>`;
} else {
if (this.isMobile()) {
difficultyPowerOfTen = selectPowerOfTen(tick.data[1]);

View File

@@ -1,5 +1,5 @@
<div [formGroup]="languageForm" class="text-small text-center">
<select formControlName="language" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 95px;" (change)="changeLanguage()">
<select formControlName="language" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 100px;" (change)="changeLanguage()">
<option *ngFor="let lang of languages" [value]="lang.code">{{ lang.name }}</option>
</select>
</div>

View File

@@ -1,20 +1,6 @@
<ng-container *ngIf="{ val: network$ | async } as network">
<header *ngIf="headerVisible" class="sticky-header">
<header *ngIf="headerVisible">
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<!-- Hamburger -->
<ng-container *ngIf="servicesEnabled">
<div *ngIf="user" class="profile_image_container" [class]="{'anon': !user.imageMd5}" (click)="hamburgerClick($event)">
<img *ngIf="user.imageMd5" [src]="'/api/v1/services/account/image/' + user.username + '?md5=' + user.imageMd5" class="profile_image">
<app-svg-images style="color: lightgrey; fill: lightgray" *ngIf="!user.imageMd5" name="anon"></app-svg-images>
</div>
<div *ngIf="false && user === null" class="profile_image_container" (click)="hamburgerClick($event)">
<app-svg-images name="hamburger" height="40"></app-svg-images>
</div>
<!-- Empty placeholder -->
<div *ngIf="user === undefined" class="profile_image_container"></div>
</ng-container>
<a class="navbar-brand" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
<ng-template [ngIf]="subdomain">
<div class="subdomain_container">
@@ -76,19 +62,11 @@
</nav>
</header>
<div class="d-flex" style="overflow: clip">
<app-menu *ngIf="servicesEnabled" [navOpen]="menuOpen" (loggedOut)="onLoggedOut()" (menuToggled)="menuToggled($event)"></app-menu>
<app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'signet'"></app-testnet-alert>
<div class="flex-grow-1 d-flex flex-column">
<app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'signet'"></app-testnet-alert>
<main style="min-width: 375px" [style]="menuOpen ? 'max-width: calc(100vw - 225px)' : 'max-width: 100vw'">
<router-outlet></router-outlet>
</main>
<div class="flex-grow-1"></div>
<app-global-footer *ngIf="footerVisible"></app-global-footer>
</div>
</div>
<main>
<router-outlet></router-outlet>
</main>
<app-global-footer *ngIf="footerVisible"></app-global-footer>
</ng-container>

View File

@@ -1,11 +1,3 @@
.sticky-header {
position: sticky;
position: -webkit-sticky;
top: 0;
width: 100%;
z-index: 100;
}
li.nav-item.active {
background-color: #653b9c;
}
@@ -94,6 +86,7 @@ li.nav-item {
.navbar-brand {
position: relative;
height: 65px;
}
.navbar-brand.dual-logos {
@@ -109,7 +102,7 @@ nav {
.connection-badge {
position: absolute;
top: 12px;
top: 22px;
width: 100%;
}
@@ -216,26 +209,4 @@ nav {
margin-left: 5px;
margin-right: 0px;
}
}
.profile_image_container {
width: 35px;
margin-right: 15px;
text-align: center;
align-self: center;
cursor: pointer;
&.anon {
border: 1.5px solid lightgrey;
color: lightgrey;
border-radius: 5px;
}
}
.profile_image {
height: 35px;
border-radius: 5px;
}
main {
transition: 0.2s;
transition-property: max-width;
}

View File

@@ -1,13 +1,9 @@
import { Component, OnInit, Input, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { Component, OnInit, Input } from '@angular/core';
import { Env, StateService } from '../../services/state.service';
import { Observable, merge, of } from 'rxjs';
import { LanguageService } from '../../services/language.service';
import { EnterpriseService } from '../../services/enterprise.service';
import { NavigationService } from '../../services/navigation.service';
import { MenuComponent } from '../menu/menu.component';
import { StorageService } from '../../services/storage.service';
import { ApiService } from '../../services/api.service';
@Component({
selector: 'app-master-page',
@@ -29,21 +25,12 @@ export class MasterPageComponent implements OnInit {
networkPaths: { [network: string]: string };
networkPaths$: Observable<Record<string, string>>;
footerVisible = true;
user: any = undefined;
servicesEnabled = false;
menuOpen = false;
@ViewChild(MenuComponent)
public menuComponent!: MenuComponent;
constructor(
public stateService: StateService,
private languageService: LanguageService,
private enterpriseService: EnterpriseService,
private navigationService: NavigationService,
private storageService: StorageService,
private apiService: ApiService,
private router: Router,
) { }
ngOnInit(): void {
@@ -64,47 +51,17 @@ export class MasterPageComponent implements OnInit {
this.footerVisible = this.footerVisibleOverride;
}
});
this.servicesEnabled = this.officialMempoolSpace && this.stateService.env.ACCELERATOR === true && this.stateService.network === '';
this.refreshAuth();
const isServicesPage = this.router.url.includes('/services/');
this.menuOpen = isServicesPage && !this.isSmallScreen();
}
collapse(): void {
this.navCollapsed = !this.navCollapsed;
}
isSmallScreen() {
return window.innerWidth <= 767.98;
}
onResize(): void {
this.isMobile = this.isSmallScreen();
this.isMobile = window.innerWidth <= 767.98;
}
brandClick(e): void {
this.stateService.resetScroll$.next(true);
}
onLoggedOut(): void {
this.refreshAuth();
}
refreshAuth(): void {
this.user = this.storageService.getAuth()?.user ?? null;
}
hamburgerClick(event): void {
if (this.menuComponent) {
this.menuComponent.hamburgerClick();
this.menuOpen = this.menuComponent.navOpen;
event.stopPropagation();
}
}
menuToggled(isOpen: boolean): void {
this.menuOpen = isOpen;
}
}

View File

@@ -5,7 +5,6 @@ import { switchMap, map, tap, filter } from 'rxjs/operators';
import { MempoolBlock, TransactionStripped } from '../../interfaces/websocket.interface';
import { Observable, BehaviorSubject } from 'rxjs';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { WebsocketService } from '../../services/websocket.service';
@Component({
@@ -55,7 +54,6 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
const ordinal = this.getOrdinal(mempoolBlocks[this.mempoolBlockIndex]);
this.ordinal$.next(ordinal);
this.seoService.setTitle(ordinal);
this.seoService.setDescription($localize`:@@meta.description.mempool-block:See stats for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} transactions in the mempool: fee range, aggregate size, and more. Mempool blocks are updated in real-time as the network receives new transactions.`);
mempoolBlocks[this.mempoolBlockIndex].isStack = mempoolBlocks[this.mempoolBlockIndex].blockVSize > this.stateService.blockVSize;
return mempoolBlocks[this.mempoolBlockIndex];
})

View File

@@ -97,10 +97,6 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
ngOnInit() {
this.chainTip = this.stateService.latestBlockHeight;
const width = this.containerOffset + (this.stateService.env.MEMPOOL_BLOCKS_AMOUNT) * this.blockOffset;
this.mempoolWidth = width;
this.widthChange.emit(this.mempoolWidth);
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
this.enabledMiningInfoIfNeeded(this.location.path());
this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
@@ -165,11 +161,11 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
return this.mempoolBlocks;
}),
tap(() => {
this.cd.markForCheck();
const width = this.containerOffset + this.mempoolBlocks.length * this.blockOffset;
if (this.mempoolWidth !== width) {
this.mempoolWidth = width;
this.widthChange.emit(this.mempoolWidth);
this.cd.markForCheck();
}
})
);
@@ -219,13 +215,11 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
if (isNewBlock && (block?.extras?.similarity == null || block?.extras?.similarity > 0.5) && !this.tabHidden) {
this.blockIndex++;
}
this.cd.markForCheck();
});
this.chainTipSubscription = this.stateService.chainTip$.subscribe((height) => {
if (this.chainTip === -1) {
this.chainTip = height;
this.cd.markForCheck();
}
});
@@ -263,7 +257,6 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.blockPadding = 0.24 * this.blockWidth;
this.containerOffset = 0.32 * this.blockWidth;
this.blockOffset = this.blockWidth + this.blockPadding;
this.cd.markForCheck();
}
}
@@ -282,7 +275,6 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
onResize(): void {
this.animateEntry = false;
this.reduceEmptyBlocksToFitScreen(this.mempoolEmptyBlocks);
this.cd.markForCheck();
}
trackByFn(index: number, block: MempoolBlock) {

View File

@@ -1,7 +1,6 @@
import { Component, OnInit, Input, Inject, LOCALE_ID, ChangeDetectionStrategy, OnChanges } from '@angular/core';
import { VbytesPipe } from '../../shared/pipes/bytes-pipe/vbytes.pipe';
import { WuBytesPipe } from '../../shared/pipes/bytes-pipe/wubytes.pipe';
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
import { formatNumber } from '@angular/common';
import { OptimizedMempoolStats } from '../../interfaces/node-api.interface';
import { StateService } from '../../services/state.service';
@@ -27,7 +26,6 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
@Input() data: any[];
@Input() filterSize = 100000;
@Input() limitFilterFee = 1;
@Input() hideCount: boolean = false;
@Input() height: number | string = 200;
@Input() top: number | string = 20;
@Input() right: number | string = 10;
@@ -52,13 +50,10 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
inverted: boolean;
chartInstance: any = undefined;
weightMode: boolean = false;
isWidget: boolean = false;
showCount: boolean = true;
constructor(
private vbytesPipe: VbytesPipe,
private wubytesPipe: WuBytesPipe,
private amountShortenerPipe: AmountShortenerPipe,
private stateService: StateService,
private storageService: StorageService,
@Inject(LOCALE_ID) private locale: string,
@@ -67,16 +62,12 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
ngOnInit(): void {
this.isLoading = true;
this.inverted = this.storageService.getValue('inverted-graph') === 'true';
this.isWidget = this.template === 'widget';
this.showCount = !this.isWidget && !this.hideCount;
}
ngOnChanges(changes) {
ngOnChanges() {
if (!this.data) {
return;
}
this.isWidget = this.template === 'widget';
this.showCount = !this.isWidget && !this.hideCount;
this.windowPreference = this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference');
this.mempoolVsizeFeesData = this.handleNewMempoolData(this.data.concat([]));
this.mountFeeChart();
@@ -105,12 +96,10 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
mempoolStats.reverse();
const labels = mempoolStats.map(stats => stats.added);
const finalArrayVByte = this.generateArray(mempoolStats);
const finalArrayCount = this.generateCountArray(mempoolStats);
return {
labels: labels,
series: finalArrayVByte,
countSeries: finalArrayCount,
series: finalArrayVByte
};
}
@@ -135,13 +124,9 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
return finalArray;
}
generateCountArray(mempoolStats: OptimizedMempoolStats[]) {
return mempoolStats.filter(stats => stats.count > 0).map(stats => [stats.added * 1000, stats.count]);
}
mountFeeChart() {
this.orderLevels();
const { series, countSeries } = this.mempoolVsizeFeesData;
const { series } = this.mempoolVsizeFeesData;
const seriesGraph = [];
const newColors = [];
@@ -193,29 +178,6 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
});
}
}
if (this.showCount) {
newColors.push('white');
seriesGraph.push({
zlevel: 1,
yAxisIndex: 1,
name: 'count',
type: 'line',
stack: 'count',
smooth: false,
markPoint: false,
lineStyle: {
width: 2,
opacity: 1,
},
symbol: 'none',
silent: true,
areaStyle: {
color: null,
opacity: 0,
},
data: countSeries,
});
}
this.mempoolVsizeFeesOptions = {
series: this.inverted ? [...seriesGraph].reverse() : seriesGraph,
@@ -239,11 +201,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
label: {
formatter: (params: any) => {
if (params.axisDimension === 'y') {
if (params.axisIndex === 0) {
return this.vbytesPipe.transform(params.value, 2, 'vB', 'MvB', true);
} else {
return this.amountShortenerPipe.transform(params.value, 2, undefined, true);
}
return this.vbytesPipe.transform(params.value, 2, 'vB', 'MvB', true)
} else {
return formatterXAxis(this.locale, this.windowPreference, params.value);
}
@@ -256,11 +214,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
const itemFormatted = [];
let totalParcial = 0;
let progressPercentageText = '';
let countItem;
let items = this.inverted ? [...params].reverse() : params;
if (items[items.length - 1].seriesName === 'count') {
countItem = items.pop();
}
const items = this.inverted ? [...params].reverse() : params;
items.map((item: any, index: number) => {
totalParcial += item.value[1];
const progressPercentage = (item.value[1] / totalValue) * 100;
@@ -322,7 +276,6 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
</tr>`);
});
const classActive = (this.template === 'advanced') ? 'fees-wrapper-tooltip-chart-advanced' : '';
const titleCount = $localize`Count`;
const titleRange = $localize`Range`;
const titleSize = $localize`:@@7faaaa08f56427999f3be41df1093ce4089bbd75:Size`;
const titleSum = $localize`Sum`;
@@ -333,25 +286,6 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
${this.vbytesPipe.transform(totalValue, 2, 'vB', 'MvB', false)}
</span>
</div>
` +
(this.showCount && countItem ? `
<table class="count">
<tbody>
<tr class="item">
<td class="indicator-container">
<span class="indicator" style="background-color: white"></span>
<span>
${titleCount}
</span>
</td>
<td style="text-align: right;">
<span>${this.amountShortenerPipe.transform(countItem.value[1], 2, undefined, true)}</span>
</td>
</tr>
</tbody>
</table>
` : '')
+ `
<table>
<thead>
<tr>
@@ -371,12 +305,12 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
</div>`;
}
},
dataZoom: (this.isWidget && this.isMobile()) ? null : [{
dataZoom: (this.template === 'widget' && this.isMobile()) ? null : [{
type: 'inside',
realtime: true,
zoomLock: (this.isWidget) ? true : false,
zoomLock: (this.template === 'widget') ? true : false,
zoomOnMouseWheel: (this.template === 'advanced') ? true : false,
moveOnMouseMove: (this.isWidget) ? true : false,
moveOnMouseMove: (this.template === 'widget') ? true : false,
maxSpan: 100,
minSpan: 10,
}, {
@@ -405,7 +339,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
},
xAxis: [
{
name: this.isWidget ? '' : formatterXAxisLabel(this.locale, this.windowPreference),
name: this.template === 'widget' ? '' : formatterXAxisLabel(this.locale, this.windowPreference),
nameLocation: 'middle',
nameTextStyle: {
padding: [20, 0, 0, 0],
@@ -423,7 +357,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
},
}
],
yAxis: [{
yAxis: {
type: 'value',
axisLine: { onZero: false },
axisLabel: {
@@ -437,17 +371,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
opacity: 0.25,
}
}
}, this.showCount ? {
type: 'value',
position: 'right',
axisLine: { onZero: false },
axisLabel: {
formatter: (value: number) => (`${this.amountShortenerPipe.transform(value, 2, undefined, true)}`),
},
splitLine: {
show: false,
}
} : null],
},
};
}

View File

@@ -1,31 +0,0 @@
<div class="sidenav menu-click" [class]="navOpen ? 'open': ''">
<div class="d-flex menu-click">
<nav class="scrollable menu-click">
<span *ngIf="userAuth" class="menu-click">
<strong class="menu-click">@ {{ userAuth.user.username }}</strong>
</span>
<a *ngIf="!userAuth" class="d-flex justify-content-center align-items-center nav-link m-0 menu-click" routerLink="/login" role="tab" (click)="onLinkClick('/login')">
<fa-icon class="menu-click" [icon]="['fas', 'user-circle']" [fixedWidth]="true" style="font-size: 25px;margin-right: 15px;"></fa-icon>
<span class="menu-click" style="font-size: 20px;">Sign in</span>
</a>
<ng-container *ngIf="userMenuGroups$ | async as menuGroups">
<div class="menu-click" *ngFor="let group of menuGroups" style="height: max-content;">
<h6 class="d-flex justify-content-between align-items-center mt-4 mb-2 text-uppercase menu-click">
<span class="menu-click">{{ group.title }}</span>
</h6>
<ul class="nav flex-column menu-click" *ngFor="let item of group.items" (click)="onLinkClick(item.link)">
<li class="nav-item d-flex justify-content-start align-items-center menu-click">
<fa-icon class="menu-click" [icon]="['fas', item.faIcon]" [fixedWidth]="true"></fa-icon>
<button *ngIf="item.link === 'logout'" class="btn nav-link menu-click" role="tab" (click)="logout()">{{ item.title }}</button>
<a *ngIf="item.title !== 'Logout'" class="nav-link menu-click" [routerLink]="[item.link]" role="tab">{{ item.title }}</a>
</li>
</ul>
</div>
</ng-container>
</nav>
</div>
</div>

View File

@@ -1,48 +0,0 @@
.sidenav {
z-index: 1;
background-color: transparent;
width: 225px;
height: calc(100vh - 65px);
position: sticky;
top: 65px;
transition: 0.25s;
margin-left: -250px;
box-shadow: 5px 0px 30px 0px #000;
padding-bottom: 20px;
}
.scrollable {
overflow-x: hidden;
overflow-y: scroll;
}
.sidenav.open {
margin-left: 0px;
left: 0px;
display: block;
}
.sidenav a, button{
text-decoration: none;
color: lightgray;
margin-left: 20px;
}
.sidenav a:hover {
color: white;
}
.sidenav nav {
width: 100%;
height: calc(100vh - 65px);
background-color: #1d1f31;
padding-left: 20px;
padding-right: 20px;
padding-top: 20px;
padding-bottom: 20px;
@media (max-width: 991px) {
padding-bottom: 200px;
}
}
@media screen and (max-height: 450px) {
.sidenav a {font-size: 18px;}
}

View File

@@ -1,101 +0,0 @@
import { Component, OnInit, Input, Output, EventEmitter, HostListener, OnDestroy } from '@angular/core';
import { Observable } from 'rxjs';
import { ApiService } from '../../services/api.service';
import { MenuGroup } from '../../interfaces/services.interface';
import { StorageService } from '../../services/storage.service';
import { Router, NavigationStart } from '@angular/router';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-menu',
templateUrl: './menu.component.html',
styleUrls: ['./menu.component.scss']
})
export class MenuComponent implements OnInit, OnDestroy {
@Input() navOpen: boolean = false;
@Output() loggedOut = new EventEmitter<boolean>();
@Output() menuToggled = new EventEmitter<boolean>();
userMenuGroups$: Observable<MenuGroup[]> | undefined;
userAuth: any | undefined;
isServicesPage = false;
constructor(
private apiService: ApiService,
private storageService: StorageService,
private router: Router,
private stateService: StateService
) {}
ngOnInit(): void {
this.userAuth = this.storageService.getAuth();
if (this.stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE) {
this.userMenuGroups$ = this.apiService.getUserMenuGroups$();
}
this.isServicesPage = this.router.url.includes('/services/');
this.router.events.subscribe((event) => {
if (event instanceof NavigationStart) {
if (!this.isServicesPage) {
this.toggleMenu(false);
}
}
});
}
toggleMenu(toggled: boolean) {
this.navOpen = toggled;
this.menuToggled.emit(toggled);
}
isSmallScreen() {
return window.innerWidth <= 767.98;
}
logout(): void {
this.apiService.logout$().subscribe(() => {
this.loggedOut.emit(true);
if (this.stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE) {
this.userMenuGroups$ = this.apiService.getUserMenuGroups$();
this.router.navigateByUrl('/');
}
});
}
onLinkClick(link) {
if (!this.isServicesPage || this.isSmallScreen()) {
this.toggleMenu(false);
}
this.router.navigateByUrl(link);
}
hamburgerClick() {
this.toggleMenu(!this.navOpen);
this.stateService.menuOpen$.next(this.navOpen);
}
@HostListener('window:click', ['$event'])
onClick(event) {
const isServicesPageOnMobile = this.isServicesPage && this.isSmallScreen();
const cssClasses = event.target.className;
if (!cssClasses.indexOf) { // Click on chart or non html thingy, close the menu
if (!this.isServicesPage || isServicesPageOnMobile) {
this.toggleMenu(false);
}
return;
}
const isHamburger = cssClasses.indexOf('profile_image') !== -1;
const isMenu = cssClasses.indexOf('menu-click') !== -1;
if (!isHamburger && !isMenu && (!this.isServicesPage || isServicesPageOnMobile)) {
this.toggleMenu(false);
}
}
ngOnDestroy(): void {
this.stateService.menuOpen$.next(false);
}
}

View File

@@ -18,7 +18,6 @@ export class MiningDashboardComponent implements OnInit, AfterViewInit {
private router: Router
) {
this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Mining Dashboard`);
this.seoService.setDescription($localize`:@@meta.description.mining.dashboard:Get real-time Bitcoin mining stats like hashrate, difficulty adjustment, block rewards, pool dominance, and more.`);
}
ngOnInit(): void {
@@ -30,7 +29,7 @@ export class MiningDashboardComponent implements OnInit, AfterViewInit {
this.router.events.subscribe((e: NavigationStart) => {
if (e.type === EventType.NavigationStart) {
if (e.url.indexOf('graphs') === -1) { // The mining dashboard and the graph component are part of the same module so we can't use ngAfterViewInit in graphs.component.ts to blur the input
this.stateService.focusSearchInputDesktop();
this.stateService.focusSearchInputDesktop();
}
}
});

View File

@@ -56,7 +56,6 @@ export class PoolRankingComponent implements OnInit {
this.miningWindowPreference = '1w';
} else {
this.seoService.setTitle($localize`:@@mining.mining-pools:Mining Pools`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.pool-ranking:See the top Bitcoin mining pools ranked by number of blocks mined, over your desired timeframe.`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
}
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
@@ -117,7 +116,7 @@ export class PoolRankingComponent implements OnInit {
} else if (this.widget) {
poolShareThreshold = 1;
}
const data: object[] = [];
let totalShareOther = 0;
let totalBlockOther = 0;

View File

@@ -83,7 +83,6 @@ export class PoolPreviewComponent implements OnInit {
}
this.seoService.setTitle(poolStats.pool.name);
this.seoService.setDescription($localize`:@@meta.description.mining.pool:See mining pool stats for ${poolStats.pool.name}\: most recent mined blocks, hashrate over time, total block reward to date, known coinbase addresses, and more.`);
let regexes = '"';
for (const regex of poolStats.pool.regexes) {
regexes += regex + '", "';

View File

@@ -83,7 +83,6 @@ export class PoolComponent implements OnInit {
}),
map((poolStats) => {
this.seoService.setTitle(poolStats.pool.name);
this.seoService.setDescription($localize`:@@meta.description.mining.pool:See mining pool stats for ${poolStats.pool.name}\: most recent mined blocks, hashrate over time, total block reward to date, known coinbase addresses, and more.`);
let regexes = '"';
for (const regex of poolStats.pool.regexes) {
regexes += regex + '", "';

View File

@@ -1,6 +1,5 @@
import { Component } from '@angular/core';
import { Env, StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
@Component({
selector: 'app-privacy-policy',
@@ -12,11 +11,5 @@ export class PrivacyPolicyComponent {
constructor(
private stateService: StateService,
private seoService: SeoService,
) { }
ngOnInit(): void {
this.seoService.setTitle('Privacy Policy');
this.seoService.setDescription('Trusted third parties are security holes, as are trusted first parties...you should only trust your own self-hosted instance of The Mempool Open Source Project®.');
}
}

View File

@@ -1,9 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { ApiService } from '../../services/api.service';
import { StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
@Component({
selector: 'app-push-transaction',
@@ -19,17 +16,12 @@ export class PushTransactionComponent implements OnInit {
constructor(
private formBuilder: UntypedFormBuilder,
private apiService: ApiService,
public stateService: StateService,
private seoService: SeoService,
) { }
ngOnInit(): void {
this.pushTxForm = this.formBuilder.group({
txHash: ['', Validators.required],
});
this.seoService.setTitle($localize`:@@meta.title.push-tx:Broadcast Transaction`);
this.seoService.setDescription($localize`:@@meta.description.push-tx:Broadcast a transaction to the ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} network using the transaction's hash.`);
}
postTx() {

View File

@@ -1,5 +1,5 @@
<div [formGroup]="rateUnitForm" class="text-small text-center">
<select formControlName="rateUnits" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 95px;" (change)="changeUnits()">
<select formControlName="rateUnits" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 100px;" (change)="changeUnits()">
<option *ngFor="let unit of units" [value]="unit.name">{{ unit.label }}</option>
</select>
</div>

View File

@@ -6,8 +6,6 @@ import { WebsocketService } from '../../services/websocket.service';
import { RbfTree } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
@Component({
selector: 'app-rbf-list',
@@ -28,7 +26,6 @@ export class RbfList implements OnInit, OnDestroy {
private apiService: ApiService,
public stateService: StateService,
private websocketService: WebsocketService,
private seoService: SeoService,
) { }
ngOnInit(): void {
@@ -54,12 +51,9 @@ export class RbfList implements OnInit, OnDestroy {
this.isLoading = false;
})
);
this.seoService.setTitle($localize`:@@meta.title.rbf-list:RBF Replacements`);
this.seoService.setDescription($localize`:@@meta.description.rbf-list:See the most recent RBF replacements on the Bitcoin${seoDescriptionNetwork(this.stateService.network)} network, updated in real-time.`);
}
ngOnDestroy(): void {
this.websocketService.stopTrackRbf();
}
}
}

View File

@@ -10,25 +10,15 @@
<div *ngIf="countdown > 0" class="warning-label">{{ eventName }} in {{ countdown | number }} block{{ countdown === 1 ? '' : 's' }}!</div>
<div class="blockchain-wrapper" [class.time-ltr]="timeLtr" [class.time-rtl]="!timeLtr" #blockchainWrapper>
<div class="blockchain-wrapper" [class.time-ltr]="timeLtr" [class.time-rtl]="!timeLtr">
<div id="blockchain-container" [dir]="timeLtr ? 'rtl' : 'ltr'" #blockchainContainer
[class.menu-open]="menuOpen"
[class.menu-closing]="menuSliding && !menuOpen"
(mousedown)="onMouseDown($event)"
(pointerdown)="onPointerDown($event)"
(touchmove)="onTouchMove($event)"
(dragstart)="onDragStart($event)"
(scroll)="onScroll($event)"
>
<app-blockchain
[containerWidth]="chainWidth"
[pageIndex]="pageIndex"
[pages]="pages"
[blocksPerPage]="blocksPerPage"
[minScrollWidth]="minScrollWidth"
[scrollableMempool]="true"
(mempoolOffsetChange)="onMempoolOffsetChange($event)"
></app-blockchain>
<app-blockchain [pageIndex]="pageIndex" [pages]="pages" [blocksPerPage]="blocksPerPage" [minScrollWidth]="minScrollWidth" [scrollableMempool]="true" (mempoolOffsetChange)="onMempoolOffsetChange($event)"></app-blockchain>
</div>
<div class="reset-scroll" [class.hidden]="pageIndex === 0" (click)="resetScroll()">
<fa-icon [icon]="['fas', 'circle-left']" [fixedWidth]="true"></fa-icon>

View File

@@ -6,20 +6,6 @@
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
width: calc(100% + 120px);
transform: translateX(0px);
transition: transform 0;
&.menu-open {
transform: translateX(-112.5px);
transition: transform 0.25s;
}
&.menu-closing {
transform: translateX(0px);
transition: transform 0.25s;
}
}
#blockchain-container::-webkit-scrollbar {

View File

@@ -1,4 +1,4 @@
import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Input, ChangeDetectorRef, ChangeDetectionStrategy, AfterViewChecked } from '@angular/core';
import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Input, DoCheck } from '@angular/core';
import { Subscription } from 'rxjs';
import { MarkBlockState, StateService } from '../../services/state.service';
import { specialBlocks } from '../../app.constants';
@@ -8,9 +8,8 @@ import { BlockExtended } from '../../interfaces/node-api.interface';
selector: 'app-start',
templateUrl: './start.component.html',
styleUrls: ['./start.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
export class StartComponent implements OnInit, OnDestroy, DoCheck {
@Input() showLoadingIndicator = false;
interval = 60;
@@ -24,15 +23,13 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
timeLtrSubscription: Subscription;
timeLtr: boolean = this.stateService.timeLtr.value;
chainTipSubscription: Subscription;
chainTip: number = 100;
chainTip: number = -1;
tipIsSet: boolean = false;
lastMark: MarkBlockState;
markBlockSubscription: Subscription;
blockCounterSubscription: Subscription;
@ViewChild('blockchainWrapper', { static: true }) blockchainWrapper: ElementRef;
@ViewChild('blockchainContainer') blockchainContainer: ElementRef;
resetScrollSubscription: Subscription;
menuSubscription: Subscription;
resetScrollSubscription: Subscription;
isMobile: boolean = false;
isiOS: boolean = false;
@@ -42,8 +39,7 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
blocksPerPage: number = 1;
pageWidth: number;
firstPageWidth: number;
minScrollWidth: number = 40 + (155 * (8 + (2 * Math.ceil(window.innerWidth / 155))));
currentScrollWidth: number = null;
minScrollWidth: number;
pageIndex: number = 0;
pages: any[] = [];
pendingMark: number | null = null;
@@ -51,29 +47,28 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
lastUpdate: number = 0;
lastMouseX: number;
velocity: number = 0;
mempoolOffset: number = null;
mempoolWidth: number = 0;
scrollLeft: number = null;
chainWidth: number = window.innerWidth;
menuOpen: boolean = false;
menuSliding: boolean = false;
menuTimeout: number;
mempoolOffset: number = 0;
constructor(
private stateService: StateService,
private cd: ChangeDetectorRef,
) {
this.isiOS = ['iPhone','iPod','iPad'].includes((navigator as any)?.userAgentData?.platform || navigator.platform);
}
ngDoCheck(): void {
if (this.pendingOffset != null) {
const offset = this.pendingOffset;
this.pendingOffset = null;
this.addConvertedScrollOffset(offset);
}
}
ngOnInit() {
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
this.blockCounterSubscription = this.stateService.blocks$.subscribe((blocks) => {
this.blockCount = blocks.length;
this.dynamicBlocksAmount = Math.min(this.blockCount, this.stateService.env.KEEP_BLOCKS_AMOUNT, 8);
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
this.minScrollWidth = 40 + (8 * this.blockWidth) + (this.pageWidth * 2);
if (this.blockCount <= Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT)) {
this.onResize();
}
@@ -119,7 +114,7 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
this.scrollToBlock(scrollToHeight);
}
}
if (!this.tipIsSet || (blockHeight < 0 && this.mempoolOffset == null)) {
if (!this.tipIsSet || (blockHeight < 0 && !this.mempoolOffset)) {
this.pendingMark = blockHeight;
}
}
@@ -156,56 +151,17 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
this.stateService.resetScroll$.next(false);
}
});
this.menuSubscription = this.stateService.menuOpen$.subscribe((open) => {
if (this.menuOpen !== open) {
this.menuOpen = open;
this.applyMenuScroll(this.menuOpen);
}
});
}
ngAfterViewChecked(): void {
if (this.currentScrollWidth !== this.blockchainContainer?.nativeElement?.scrollWidth) {
this.currentScrollWidth = this.blockchainContainer?.nativeElement?.scrollWidth;
if (this.pendingOffset != null) {
const delta = this.pendingOffset - (this.mempoolOffset || 0);
this.mempoolOffset = this.pendingOffset;
this.currentScrollWidth = this.blockchainContainer?.nativeElement?.scrollWidth;
this.pendingOffset = null;
this.addConvertedScrollOffset(delta);
this.applyPendingMarkArrow();
} else {
this.applyScrollLeft();
}
}
}
onMempoolOffsetChange(offset): void {
if (offset !== this.mempoolOffset) {
this.pendingOffset = offset;
}
}
applyScrollLeft(): void {
if (this.blockchainContainer?.nativeElement?.scrollWidth) {
let lastScrollLeft = null;
while (this.scrollLeft < 0 && this.shiftPagesForward() && lastScrollLeft !== this.scrollLeft) {
lastScrollLeft = this.scrollLeft;
this.scrollLeft += this.pageWidth;
}
lastScrollLeft = null;
while (this.scrollLeft > this.blockchainContainer.nativeElement.scrollWidth && this.shiftPagesBack() && lastScrollLeft !== this.scrollLeft) {
lastScrollLeft = this.scrollLeft;
this.scrollLeft -= this.pageWidth;
}
this.blockchainContainer.nativeElement.scrollLeft = this.scrollLeft;
}
this.cd.detectChanges();
const delta = offset - this.mempoolOffset;
this.addConvertedScrollOffset(delta);
this.mempoolOffset = offset;
this.applyPendingMarkArrow();
}
applyPendingMarkArrow(): void {
if (this.pendingMark != null && this.pendingMark <= this.chainTip) {
if (this.pendingMark != null) {
if (this.pendingMark < 0) {
this.scrollToBlock(this.chainTip - this.pendingMark);
} else {
@@ -215,48 +171,39 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
}
}
applyMenuScroll(opening: boolean): void {
this.menuSliding = true;
window.clearTimeout(this.menuTimeout);
this.menuTimeout = window.setTimeout(() => {
this.menuSliding = false;
this.cd.markForCheck();
}, 300);
}
@HostListener('window:resize', ['$event'])
onResize(): void {
this.chainWidth = window.innerWidth;
this.isMobile = this.chainWidth <= 767.98;
this.isMobile = window.innerWidth <= 767.98;
let firstVisibleBlock;
let offset;
this.pages.forEach(page => {
const left = page.offset - this.getConvertedScrollOffset(this.scrollLeft);
const right = left + this.pageWidth;
if (left <= 0 && right > 0) {
const blockIndex = Math.max(0, Math.floor(left / -this.blockWidth));
firstVisibleBlock = page.height - blockIndex;
offset = left + (blockIndex * this.blockWidth);
}
});
if (this.blockchainContainer?.nativeElement != null) {
this.pages.forEach(page => {
const left = page.offset - this.getConvertedScrollOffset();
const right = left + this.pageWidth;
if (left <= 0 && right > 0) {
const blockIndex = Math.max(0, Math.floor(left / -this.blockWidth));
firstVisibleBlock = page.height - blockIndex;
offset = left + (blockIndex * this.blockWidth);
}
});
}
this.blocksPerPage = Math.ceil(this.chainWidth / this.blockWidth);
this.blocksPerPage = Math.ceil(window.innerWidth / this.blockWidth);
this.pageWidth = this.blocksPerPage * this.blockWidth;
this.minScrollWidth = 40 + (8 * this.blockWidth) + (this.pageWidth * 2);
this.minScrollWidth = this.firstPageWidth + (this.pageWidth * 2);
if (firstVisibleBlock != null) {
this.scrollToBlock(firstVisibleBlock, offset);
this.scrollToBlock(firstVisibleBlock, offset + (this.isMobile ? this.blockWidth : 0));
} else {
this.updatePages();
}
this.cd.markForCheck();
}
onMouseDown(event: MouseEvent) {
if (!(event.which > 1 || event.button > 0)) {
this.mouseDragStartX = event.clientX;
this.resetMomentum(event.clientX);
this.blockchainScrollLeftInit = this.scrollLeft;
this.blockchainScrollLeftInit = this.blockchainContainer.nativeElement.scrollLeft;
}
}
onPointerDown(event: PointerEvent) {
@@ -282,8 +229,8 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
if (this.mouseDragStartX != null) {
this.updateVelocity(event.clientX);
this.stateService.setBlockScrollingInProgress(true);
this.scrollLeft = this.blockchainScrollLeftInit + this.mouseDragStartX - event.clientX;
this.applyScrollLeft();
this.blockchainContainer.nativeElement.scrollLeft =
this.blockchainScrollLeftInit + this.mouseDragStartX - event.clientX;
}
}
@HostListener('document:mouseup', [])
@@ -339,31 +286,25 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
} else {
this.velocity += dv;
}
this.scrollLeft -= displacement;
this.applyScrollLeft();
this.blockchainContainer.nativeElement.scrollLeft -= displacement;
this.animateMomentum();
}
});
}
onScroll(e) {
if (this.blockchainContainer?.nativeElement?.scrollLeft == null) {
return;
}
this.scrollLeft = this.blockchainContainer?.nativeElement?.scrollLeft;
const middlePage = this.pageIndex === 0 ? this.pages[0] : this.pages[1];
// compensate for css transform
const translation = (this.isMobile ? this.chainWidth * 0.95 : this.chainWidth * 0.5);
const translation = (this.isMobile ? window.innerWidth * 0.95 : window.innerWidth * 0.5);
const backThreshold = middlePage.offset + (this.pageWidth * 0.5) + translation;
const forwardThreshold = middlePage.offset - (this.pageWidth * 0.5) + translation;
this.scrollLeft = this.blockchainContainer.nativeElement.scrollLeft;
const offsetScroll = this.getConvertedScrollOffset(this.scrollLeft);
if (offsetScroll > backThreshold) {
const scrollLeft = this.getConvertedScrollOffset();
if (scrollLeft > backThreshold) {
if (this.shiftPagesBack()) {
this.addConvertedScrollOffset(-this.pageWidth);
this.blockchainScrollLeftInit -= this.pageWidth;
}
} else if (offsetScroll < forwardThreshold) {
} else if (scrollLeft < forwardThreshold) {
if (this.shiftPagesForward()) {
this.addConvertedScrollOffset(this.pageWidth);
this.blockchainScrollLeftInit += this.pageWidth;
@@ -372,6 +313,10 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
}
scrollToBlock(height, blockOffset = 0) {
if (!this.blockchainContainer?.nativeElement) {
setTimeout(() => { this.scrollToBlock(height, blockOffset); }, 50);
return;
}
if (this.isMobile) {
blockOffset -= this.blockWidth;
}
@@ -379,15 +324,15 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
const pages = [];
this.pageIndex = Math.max(viewingPageIndex - 1, 0);
let viewingPage = this.getPageAt(viewingPageIndex);
const isLastPage = viewingPage.height <= 0;
const isLastPage = viewingPage.height < this.blocksPerPage;
if (isLastPage) {
this.pageIndex = Math.max(viewingPageIndex - 2, 0);
viewingPage = this.getPageAt(viewingPageIndex);
}
const left = viewingPage.offset - this.getConvertedScrollOffset(this.scrollLeft);
const left = viewingPage.offset - this.getConvertedScrollOffset();
const blockIndex = viewingPage.height - height;
const targetOffset = (this.blockWidth * blockIndex) + left;
const deltaOffset = targetOffset - blockOffset;
let deltaOffset = targetOffset - blockOffset;
if (isLastPage) {
pages.push(this.getPageAt(viewingPageIndex - 2));
@@ -417,7 +362,6 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
pages.push(this.getPageAt(this.pageIndex + 1));
pages.push(this.getPageAt(this.pageIndex + 2));
this.pages = pages;
this.cd.markForCheck();
}
shiftPagesBack(): boolean {
@@ -470,46 +414,49 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
blockInViewport(height: number): boolean {
const firstHeight = this.pages[0].height;
const translation = (this.isMobile ? this.chainWidth * 0.95 : this.chainWidth * 0.5);
const firstX = this.pages[0].offset - this.getConvertedScrollOffset(this.scrollLeft) + translation;
const translation = (this.isMobile ? window.innerWidth * 0.95 : window.innerWidth * 0.5);
const firstX = this.pages[0].offset - this.getConvertedScrollOffset() + translation;
const xPos = firstX + ((firstHeight - height) * 155);
return xPos > -55 && xPos < (this.chainWidth - 100);
return xPos > -55 && xPos < (window.innerWidth - 100);
}
getConvertedScrollOffset(scrollLeft): number {
getConvertedScrollOffset(): number {
if (this.timeLtr) {
return -(scrollLeft || 0) - (this.mempoolOffset || 0);
return -(this.blockchainContainer?.nativeElement?.scrollLeft || 0) - this.mempoolOffset;
} else {
return (scrollLeft || 0) - (this.mempoolOffset || 0);
return (this.blockchainContainer?.nativeElement?.scrollLeft || 0) - this.mempoolOffset;
}
}
setScrollLeft(offset: number): void {
if (this.timeLtr) {
this.scrollLeft = offset - (this.mempoolOffset || 0);
this.blockchainContainer.nativeElement.scrollLeft = offset - this.mempoolOffset;
} else {
this.scrollLeft = offset + (this.mempoolOffset || 0);
this.blockchainContainer.nativeElement.scrollLeft = offset + this.mempoolOffset;
}
this.applyScrollLeft();
}
addConvertedScrollOffset(offset: number): void {
if (this.timeLtr) {
this.scrollLeft -= offset;
} else {
this.scrollLeft += offset;
if (!this.blockchainContainer?.nativeElement) {
this.pendingOffset = offset;
return;
}
if (this.timeLtr) {
this.blockchainContainer.nativeElement.scrollLeft -= offset;
} else {
this.blockchainContainer.nativeElement.scrollLeft += offset;
}
this.applyScrollLeft();
}
ngOnDestroy() {
// clean up scroll position to prevent caching wrong scroll in Firefox
this.setScrollLeft(0);
if (this.blockchainContainer?.nativeElement) {
// clean up scroll position to prevent caching wrong scroll in Firefox
this.setScrollLeft(0);
}
this.timeLtrSubscription.unsubscribe();
this.chainTipSubscription.unsubscribe();
this.markBlockSubscription.unsubscribe();
this.blockCounterSubscription.unsubscribe();
this.resetScrollSubscription.unsubscribe();
this.menuSubscription.unsubscribe();
}
}

View File

@@ -69,12 +69,6 @@
</button>
<div class="dropdown-fees" ngbDropdownMenu aria-labelledby="dropdownFees">
<ul>
<li (click)="this.showCount = !this.showCount"
[class]="this.showCount ? '' : 'inactive'">
<span class="square" [ngStyle]="{'backgroundColor': 'white'}"></span>
<span class="fee-text">{{ titleCount }}</span>
</li>
<hr style="margin: 4px;">
<ng-template ngFor let-feeData let-i="index" [ngForOf]="feeLevelDropdownData">
<ng-template [ngIf]="feeData.fee <= (feeLevels[maxFeeIndex])">
<li (click)="filterFeeIndex = feeData.fee"
@@ -98,8 +92,8 @@
</div>
<div class="card-body">
<div class="incoming-transactions-graph">
<app-mempool-graph #mempoolgraph dir="ltr" [template]="'advanced'" [hideCount]="!showCount"
[limitFilterFee]="filterFeeIndex" [height]="500" [left]="65" [right]="showCount ? 50 : 10"
<app-mempool-graph #mempoolgraph dir="ltr" [template]="'advanced'"
[limitFilterFee]="filterFeeIndex" [height]="500" [left]="65" [right]="10"
[data]="mempoolStats && mempoolStats.length ? mempoolStats : null"></app-mempool-graph>
</div>
</div>

View File

@@ -32,7 +32,6 @@ export class StatisticsComponent implements OnInit {
chartColors = chartColors;
filterSize = 100000;
filterFeeIndex = 1;
showCount = true;
maxFeeIndex: number;
dropDownOpen = false;
@@ -47,7 +46,6 @@ export class StatisticsComponent implements OnInit {
inverted: boolean;
feeLevelDropdownData = [];
timespan = '';
titleCount = $localize`Count`;
constructor(
@Inject(LOCALE_ID) private locale: string,
@@ -64,7 +62,6 @@ export class StatisticsComponent implements OnInit {
this.inverted = this.storageService.getValue('inverted-graph') === 'true';
this.setFeeLevelDropdownData();
this.seoService.setTitle($localize`:@@5d4f792f048fcaa6df5948575d7cb325c9393383:Graphs`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.mempool:See mempool size (in MvB) and transactions per second (in vB/s) visualized over time.`);
this.stateService.networkChanged$.subscribe((network) => this.network = network);
this.graphWindowPreference = this.storageService.getValue('graphWindowPreference') ? this.storageService.getValue('graphWindowPreference').trim() : '2h';

View File

@@ -74,16 +74,6 @@
<path fill="#FFFFFF" d="M128 768h256v64H128v-64z m320-384H128v64h320v-64z m128 192V448L384 640l192 192V704h320V576H576z m-288-64H128v64h160v-64zM128 704h160v-64H128v64z m576 64h64v128c-1 18-7 33-19 45s-27 18-45 19H64c-35 0-64-29-64-64V192c0-35 29-64 64-64h192C256 57 313 0 384 0s128 57 128 128h192c35 0 64 29 64 64v320h-64V320H64v576h640V768zM128 256h512c0-35-29-64-64-64h-64c-35 0-64-29-64-64s-29-64-64-64-64 29-64 64-29 64-64 64h-64c-35 0-64 29-64 64z" />
</svg>
</ng-container>
<ng-container *ngSwitchCase="'hamburger'">
<svg [attr.width]="width" [attr.height]="height" viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg" stroke="currentColor">
<path stroke-width="0.5" stroke-linecap="round" d="M0.5 2.5 H7 M0.5 5 H5.5 M0.5 7.5 H7"></path>
</svg>
</ng-container>
<ng-container *ngSwitchCase="'anon'">
<svg [attr.width]="width" [attr.height]="height" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 9.5v-2a3 3 0 116 0v2c0 1.11-.603 2.08-1.5 2.599v1.224a1 1 0 00.629.928l2.05.82A3.693 3.693 0 0118.5 18.5h-13c0-1.51.92-2.868 2.321-3.428l2.05-.82a1 1 0 00.629-.929v-1.224A2.999 2.999 0 019 9.5z"></path>
</svg>
</ng-container>
</ng-container>
<ng-template #bitcoinLogo let-color let-width="width" let-height="height" let-viewBox="viewBox">

View File

@@ -37,7 +37,6 @@ export class TelevisionComponent implements OnInit, OnDestroy {
ngOnInit() {
this.seoService.setTitle($localize`:@@46ce8155c9ab953edeec97e8950b5a21e67d7c4e:TV view`);
this.seoService.setDescription($localize`:@@meta.description.tv:See Bitcoin blocks and mempool congestion in real-time in a simplified format perfect for a TV.`);
this.websocketService.want(['blocks', 'live-2h-chart', 'mempool-blocks']);
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {

View File

@@ -1,6 +1,5 @@
import { Component } from '@angular/core';
import { Env, StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
@Component({
selector: 'app-terms-of-service',
@@ -11,11 +10,5 @@ export class TermsOfServiceComponent {
constructor(
private stateService: StateService,
private seoService: SeoService,
) { }
ngOnInit(): void {
this.seoService.setTitle('Terms of Service');
this.seoService.setDescription('Out of respect for the Bitcoin community, the mempool.space website is Bitcoin Only and does not display any advertising.');
}
}

View File

@@ -1,6 +1,5 @@
import { Component } from '@angular/core';
import { Env, StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
@Component({
selector: 'app-trademark-policy',
@@ -12,11 +11,5 @@ export class TrademarkPolicyComponent {
constructor(
private stateService: StateService,
private seoService: SeoService,
) { }
ngOnInit(): void {
this.seoService.setTitle('Trademark Policy');
this.seoService.setDescription('An overview of the trademarks registered by Mempool Space K.K. and The Mempool Open Source Project® and what we consider to be lawful usage of those trademarks.');
}
}

View File

@@ -15,7 +15,6 @@ import { CacheService } from '../../services/cache.service';
import { OpenGraphService } from '../../services/opengraph.service';
import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { CpfpInfo } from '../../interfaces/node-api.interface';
import { LiquidUnblinding } from './liquid-ublinding';
@@ -88,7 +87,6 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
this.seoService.setTitle(
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.transaction:Get real-time status, addresses, fees, script info, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} transaction with txid {txid}.`);
this.resetTransaction();
return merge(
of(true),

View File

@@ -6,13 +6,6 @@
<app-truncate [text]="rbfTransaction.txid" [lastChars]="12" [link]="['/tx/' | relativeUrl, rbfTransaction.txid]"></app-truncate>
</div>
<div *ngIf="acceleratorAvailable && accelerateCtaType === 'alert' && !tx?.status?.confirmed && !tx?.acceleration" class="alert alert-mempool alert-dismissible" role="alert">
<span><a class="link accelerator" (click)="onAccelerateClicked()">Accelerate</a> this transaction using Mempool Accelerator &trade;</span>
<button type="button" class="close" aria-label="Close" (click)="dismissAccelAlert()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<ng-container *ngIf="!rbfTransaction || rbfTransaction?.size || tx">
<h1 i18n="shared.transaction">Transaction</h1>
@@ -73,22 +66,12 @@
<div class="col-sm">
<ng-container *ngTemplateOutlet="feeTable"></ng-container>
</div>
</div>
</div>
</ng-template>
<!-- Accelerator -->
<ng-container *ngIf="!tx?.status?.confirmed && showAccelerationSummary">
<div class="title mt-3" id="acceleratePreviewAnchor">
<h2>Accelerate</h2>
</div>
<div class="box">
<app-accelerate-preview [tx]="tx" [scrollEvent]="scrollIntoAccelPreview"></app-accelerate-preview>
</div>
</ng-container>
<ng-template #unconfirmedTemplate>
<div class="box">
@@ -109,16 +92,16 @@
</ng-template>
</ng-template>
<tr *ngIf="!replaced && !isCached">
<td class="td-width align-items-center align-middle" i18n="transaction.eta|Transaction ETA">ETA</td>
<td class="td-width" i18n="transaction.eta|Transaction ETA">ETA</td>
<td>
<ng-template [ngIf]="this.mempoolPosition?.block == null" [ngIfElse]="estimationTmpl">
<span class="skeleton-loader"></span>
</ng-template>
<ng-template #estimationTmpl>
<ng-template [ngIf]="this.mempoolPosition.block >= 7" [ngIfElse]="belowBlockLimit">
<span [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'etaDeepMempool d-flex justify-content-end align-items-center' : ''">
<span class="eta d-flex">
<span i18n="transaction.eta.in-several-hours|Transaction ETA in several hours or more">In several hours (or more)</span>
<a *ngIf="acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerateDeepMempool" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
<span class="ml-2"></span><a *ngIf="stateService.env.OFFICIAL_MEMPOOL_SPACE && stateService.env.ACCELERATOR && stateService.network === ''" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn badge badge-primary accelerate ml-auto" i18n="transaction.accelerate|Accelerate button label">Accelerate</a>
</span>
</ng-template>
<ng-template #belowBlockLimit>
@@ -126,9 +109,9 @@
<app-time kind="until" [time]="(60 * 1000 * this.mempoolPosition.block) + now" [fastRender]="false" [fixedRender]="true"></app-time>
</ng-template>
<ng-template #timeEstimateDefault>
<span class="eta justify-content-end" [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'd-flex align-items-center' : ''">
<span class="d-flex">
<app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.timeAvg * (this.mempoolPosition.block + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
<a *ngIf="acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerate" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
<span class="ml-2"></span><a *ngIf="stateService.env.OFFICIAL_MEMPOOL_SPACE && stateService.env.ACCELERATOR && stateService.network === ''" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn badge badge-primary accelerate ml-auto" i18n="transaction.accelerate|Accelerate button label">Accelerate</a>
</span>
</ng-template>
</ng-template>

View File

@@ -130,7 +130,7 @@
}
.table {
tr td {
tr td {
padding: 0.75rem 0.5rem;
@media (min-width: 576px) {
padding: 0.75rem 0.75rem;
@@ -138,7 +138,7 @@
&:last-child {
text-align: right;
@media (min-width: 850px) {
text-align: left;
text-align: left;
}
}
.btn {
@@ -218,52 +218,21 @@
}
}
.link.accelerator {
cursor: pointer;
}
.eta {
display: flex;
justify-content: end;
flex-wrap: wrap;
align-content: center;
@media (min-width: 850px) {
justify-content: left !important;
justify-content: space-between;
}
}
.accelerate {
display: flex !important;
align-self: auto;
margin-top: 3px;
margin-left: auto;
background-color: #653b9c;
@media (max-width: 849px) {
margin-left: 5px;
}
}
.etaDeepMempool {
display: flex !important;
justify-content: end;
flex-wrap: wrap;
align-content: center;
@media (max-width: 995px) {
justify-content: left !important;
}
@media (max-width: 849px) {
justify-content: right !important;
}
}
.accelerateDeepMempool {
align-self: auto;
margin-top: 3px;
margin-left: auto;
background-color: #653b9c;
@media (max-width: 995px) {
@media (min-width: 850px) {
justify-self: start;
margin-left: 0px;
}
@media (max-width: 849px) {
margin-left: 5px;
}
}
}

View File

@@ -19,8 +19,6 @@ import { WebsocketService } from '../../services/websocket.service';
import { AudioService } from '../../services/audio.service';
import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service';
import { StorageService } from '../../services/storage.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment } from '../../interfaces/node-api.interface';
import { LiquidUnblinding } from './liquid-ublinding';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
@@ -90,10 +88,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
rbfEnabled: boolean;
taprootEnabled: boolean;
hasEffectiveFeeRate: boolean;
accelerateCtaType: 'alert' | 'button' = 'alert';
acceleratorAvailable: boolean = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
showAccelerationSummary = false;
scrollIntoAccelPreview = false;
@ViewChild('graphContainer')
graphContainer: ElementRef;
@@ -110,22 +104,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
private apiService: ApiService,
private seoService: SeoService,
private priceService: PriceService,
private storageService: StorageService
) {}
ngOnInit() {
this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
this.websocketService.want(['blocks', 'mempool-blocks']);
this.stateService.networkChanged$.subscribe(
(network) => {
this.network = network;
this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
}
(network) => (this.network = network)
);
this.accelerateCtaType = (this.storageService.getValue('accel-cta-type') as 'alert' | 'button') ?? 'alert';
this.setFlowEnabled();
this.flowPrefSubscription = this.stateService.hideFlow.subscribe((hide) => {
this.hideFlow = !!hide;
@@ -175,7 +161,34 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
})
)
.subscribe((cpfpInfo) => {
this.setCpfpInfo(cpfpInfo);
if (!cpfpInfo || !this.tx) {
this.cpfpInfo = null;
this.hasEffectiveFeeRate = false;
return;
}
// merge ancestors/descendants
const relatives = [...(cpfpInfo.ancestors || []), ...(cpfpInfo.descendants || [])];
if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) {
relatives.push(cpfpInfo.bestDescendant);
}
const hasRelatives = !!relatives.length;
if (!cpfpInfo.effectiveFeePerVsize && hasRelatives) {
let totalWeight =
this.tx.weight +
relatives.reduce((prev, val) => prev + val.weight, 0);
let totalFees =
this.tx.fee +
relatives.reduce((prev, val) => prev + val.fee, 0);
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
} else {
this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
}
if (cpfpInfo.acceleration) {
this.tx.acceleration = cpfpInfo.acceleration;
}
this.cpfpInfo = cpfpInfo;
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01));
});
this.fetchRbfSubscription = this.fetchRbfHistory$
@@ -246,10 +259,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
mempoolPosition: this.mempoolPosition
});
this.txInBlockIndex = this.mempoolPosition.block;
if (txPosition.cpfp !== undefined) {
this.setCpfpInfo(txPosition.cpfp);
}
}
} else {
this.mempoolPosition = null;
@@ -288,7 +297,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.seoService.setTitle(
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.transaction:Get real-time status, addresses, fees, script info, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} transaction with txid {txid}.`);
this.resetTransaction();
return merge(
of(true),
@@ -391,7 +399,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.blockConversion = price;
})
).subscribe();
setTimeout(() => { this.applyFragment(); }, 0);
},
(error) => {
@@ -478,20 +486,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.setGraphSize();
}
dismissAccelAlert(): void {
this.storageService.setValue('accel-cta-type', 'button');
this.accelerateCtaType = 'button';
}
onAccelerateClicked() {
if (!this.txId) {
return;
}
this.showAccelerationSummary = true && this.acceleratorAvailable;
this.scrollIntoAccelPreview = !this.scrollIntoAccelPreview;
return false;
}
handleLoadElectrsTransactionError(error: any): Observable<any> {
if (error.status === 404 && /^[a-fA-F0-9]{64}$/.test(this.txId)) {
this.websocketService.startMultiTrackTransaction(this.txId);
@@ -513,37 +507,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
});
}
setCpfpInfo(cpfpInfo: CpfpInfo): void {
if (!cpfpInfo || !this.tx) {
this.cpfpInfo = null;
this.hasEffectiveFeeRate = false;
return;
}
// merge ancestors/descendants
const relatives = [...(cpfpInfo.ancestors || []), ...(cpfpInfo.descendants || [])];
if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) {
relatives.push(cpfpInfo.bestDescendant);
}
const hasRelatives = !!relatives.length;
if (!cpfpInfo.effectiveFeePerVsize && hasRelatives) {
const totalWeight =
this.tx.weight +
relatives.reduce((prev, val) => prev + val.weight, 0);
const totalFees =
this.tx.fee +
relatives.reduce((prev, val) => prev + val.fee, 0);
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
} else {
this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
}
if (cpfpInfo.acceleration) {
this.tx.acceleration = cpfpInfo.acceleration;
}
this.cpfpInfo = cpfpInfo;
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01));
}
setFeatures(): void {
if (this.tx) {
this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit');

View File

@@ -69,7 +69,6 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
ngOnInit(): void {
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
this.seoService.resetTitle();
this.seoService.resetDescription();
this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']);
this.websocketService.startTrackRbfSummary();
this.network$ = merge(of(''), this.stateService.networkChanged$);

View File

@@ -155,7 +155,7 @@ ul.no-bull.block-audit code{
#doc-nav-desktop.fixed {
float: unset;
position: fixed;
top: 80px;
top: 20px;
overflow-y: auto;
height: calc(100vh - 50px);
scrollbar-color: #2d3348 #11131f;

View File

@@ -43,7 +43,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
if (this.faqTemplates) {
this.faqTemplates.forEach((x) => this.dict[x.type] = x.template);
}
this.desktopDocsNavPosition = ( window.pageYOffset > 115 ) ? "fixed" : "relative";
this.desktopDocsNavPosition = ( window.pageYOffset > 182 ) ? "fixed" : "relative";
this.mobileViewport = window.innerWidth <= 992;
}
@@ -113,7 +113,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
}
onDocScroll() {
this.desktopDocsNavPosition = ( window.pageYOffset > 115 ) ? "fixed" : "relative";
this.desktopDocsNavPosition = ( window.pageYOffset > 182 ) ? "fixed" : "relative";
}
anchorLinkClick( event: any ) {

View File

@@ -28,6 +28,21 @@ export class DocsComponent implements OnInit {
ngOnInit(): void {
this.websocket.want(['blocks']);
const url = this.route.snapshot.url;
if (url[0].path === "faq" ) {
this.activeTab = 0;
this.seoService.setTitle($localize`:@@docs.faq.button-title:FAQ`);
} else if( url[1].path === "rest" ) {
this.activeTab = 1;
this.seoService.setTitle($localize`:@@e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4:API`);
} else if( url[1].path === "websocket" ) {
this.activeTab = 2;
this.seoService.setTitle($localize`:@@e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4:API`);
} else {
this.activeTab = 3;
this.seoService.setTitle($localize`:@@e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4:API`);
}
this.env = this.stateService.env;
this.showWebSocketTab = ( ! ( ( this.stateService.network === "bisq" ) || ( this.stateService.network === "liquidtestnet" ) ) );
this.showFaqTab = ( this.env.BASE_MODULE === 'mempool' ) ? true : false;
@@ -36,40 +51,6 @@ export class DocsComponent implements OnInit {
document.querySelector<HTMLElement>( "html" ).style.scrollBehavior = "smooth";
}
ngDoCheck(): void {
const url = this.route.snapshot.url;
if (url[0].path === "faq" ) {
this.activeTab = 0;
this.seoService.setTitle($localize`:@@meta.title.docs.faq:FAQ`);
this.seoService.setDescription($localize`:@@meta.description.docs.faq:Get answers to common questions like: What is a mempool? Why isn't my transaction confirming? How can I run my own instance of The Mempool Open Source Project? And more.`);
} else if( url[1].path === "rest" ) {
this.activeTab = 1;
this.seoService.setTitle($localize`:@@meta.title.docs.rest:REST API`);
if( this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet' ) {
this.seoService.setDescription($localize`:@@meta.description.docs.rest-liquid:Documentation for the liquid.network REST API service: get info on addresses, transactions, assets, blocks, and more.`);
} else if( this.stateService.network === 'bisq' ) {
this.seoService.setDescription($localize`:@@meta.description.docs.rest-bisq:Documentation for the bisq.markets REST API service: get info on recent trades, current offers, transactions, network state, and more.`);
} else {
this.seoService.setDescription($localize`:@@meta.description.docs.rest-bitcoin:Documentation for the mempool.space REST API service: get info on addresses, transactions, blocks, fees, mining, the Lightning network, and more.`);
}
} else if( url[1].path === "websocket" ) {
this.activeTab = 2;
this.seoService.setTitle($localize`:@@meta.title.docs.websocket:WebSocket API`);
if( this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet' ) {
this.seoService.setDescription($localize`:@@meta.description.docs.websocket-liquid:Documentation for the liquid.network WebSocket API service: get real-time info on blocks, mempools, transactions, addresses, and more.`);
} else {
this.seoService.setDescription($localize`:@@meta.description.docs.websocket-bitcoin:Documentation for the mempool.space WebSocket API service: get real-time info on blocks, mempools, transactions, addresses, and more.`);
}
} else {
this.activeTab = 3;
this.seoService.setTitle($localize`:@@meta.title.docs.websocket:Electrum RPC`);
this.seoService.setDescription($localize`:@@meta.description.docs.electrumrpc:Documentation for our Electrum RPC interface: get instant, convenient, and reliable access to an Esplora instance.`);
}
}
ngOnDestroy(): void {
document.querySelector<HTMLElement>( "html" ).style.scrollBehavior = "auto";
}

View File

@@ -19,7 +19,7 @@ export interface Transaction {
ancestors?: Ancestor[];
bestDescendant?: BestDescendant | null;
cpfpChecked?: boolean;
acceleration?: boolean;
acceleration?: number;
deleteAfter?: number;
_unblinded?: any;
_deduced?: boolean;

View File

@@ -2,7 +2,6 @@ import { Block, Transaction } from "./electrs.interface";
export interface OptimizedMempoolStats {
added: number;
count: number;
vbytes_per_second: number;
total_fee: number;
mempool_byte_weight: number;
@@ -28,7 +27,7 @@ export interface CpfpInfo {
effectiveFeePerVsize?: number;
sigops?: number;
adjustedVsize?: number;
acceleration?: boolean;
acceleration?: number;
}
export interface RbfInfo {

View File

@@ -1,13 +0,0 @@
import { IconName } from '@fortawesome/fontawesome-common-types';
export type MenuItem = {
title: string;
i18n: string;
faIcon: IconName;
link: string;
};
export type MenuGroup = {
title: string;
i18n: string;
items: MenuItem[];
}

View File

@@ -95,7 +95,7 @@ export interface TransactionStripped {
}
export interface IBackendInfo {
hostname?: string;
hostname: string;
gitCommit: string;
version: string;
}

View File

@@ -34,7 +34,6 @@ export class ChannelPreviewComponent implements OnInit {
this.openGraphService.waitFor('channel-data-' + this.shortId);
this.error = null;
this.seoService.setTitle(`Channel: ${params.get('short_id')}`);
this.seoService.setDescription($localize`:@@meta.description.lightning.channel:Overview for Lightning channel ${params.get('short_id')}. See channel capacity, the Lightning nodes involved, related on-chain transactions, and more.`);
return this.lightningApiService.getChannel$(params.get('short_id'))
.pipe(
tap((data) => {

View File

@@ -35,7 +35,6 @@ export class ChannelComponent implements OnInit {
.pipe(
tap((value) => {
this.seoService.setTitle($localize`Channel: ${value.short_id}`);
this.seoService.setDescription($localize`:@@meta.description.lightning.channel:Overview for Lightning channel ${value.short_id}. See channel capacity, the Lightning nodes involved, related on-chain transactions, and more.`);
}),
catchError((err) => {
this.error = err;

View File

@@ -31,7 +31,6 @@ export class GroupPreviewComponent implements OnInit {
ngOnInit(): void {
this.seoService.setTitle(`Mempool.Space Lightning Nodes`);
this.seoService.setDescription(`See all Lightning nodes run by mempool.space -- these are the nodes that provide the data on the mempool.space Lightning dashboard.`);
this.nodes$ = this.activatedRoute.paramMap
.pipe(

View File

@@ -39,7 +39,6 @@ export class GroupComponent implements OnInit {
});
this.seoService.setTitle(`Mempool.space Lightning Nodes`);
this.seoService.setDescription(`See all Lightning nodes run by mempool.space -- these are the nodes that provide the data on the mempool.space Lightning dashboard.`);
this.nodes$ = this.lightningApiService.getNodGroupNodes$('mempool.space')
.pipe(

Some files were not shown because too many files have changed in this diff Show More