Merge branch 'master' into natsoni/add-blocks-logo
This commit is contained in:
commit
46d99db167
@ -75,6 +75,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
@Output() changeMode = new EventEmitter<boolean>();
|
@Output() changeMode = new EventEmitter<boolean>();
|
||||||
|
|
||||||
calculating = true;
|
calculating = true;
|
||||||
|
processing = false;
|
||||||
selectedOption: 'wait' | 'accel';
|
selectedOption: 'wait' | 'accel';
|
||||||
cantPayReason = '';
|
cantPayReason = '';
|
||||||
quoteError = ''; // error fetching estimate or initial data
|
quoteError = ''; // error fetching estimate or initial data
|
||||||
@ -380,9 +381,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
* Account-based acceleration request
|
* Account-based acceleration request
|
||||||
*/
|
*/
|
||||||
accelerateWithMempoolAccount(): void {
|
accelerateWithMempoolAccount(): void {
|
||||||
if (!this.canPay || this.calculating) {
|
if (!this.canPay || this.calculating || this.processing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.processing = true;
|
||||||
if (this.accelerationSubscription) {
|
if (this.accelerationSubscription) {
|
||||||
this.accelerationSubscription.unsubscribe();
|
this.accelerationSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
@ -392,6 +394,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.accelerationUUID
|
this.accelerationUUID
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
|
this.processing = false;
|
||||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
this.showSuccess = true;
|
this.showSuccess = true;
|
||||||
@ -399,6 +402,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.moveToStep('paid');
|
this.moveToStep('paid');
|
||||||
},
|
},
|
||||||
error: (response) => {
|
error: (response) => {
|
||||||
|
this.processing = false;
|
||||||
this.accelerateError = response.error;
|
this.accelerateError = response.error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -468,10 +472,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
* APPLE PAY
|
* APPLE PAY
|
||||||
*/
|
*/
|
||||||
async requestApplePayPayment(): Promise<void> {
|
async requestApplePayPayment(): Promise<void> {
|
||||||
|
if (this.processing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.conversionsSubscription) {
|
if (this.conversionsSubscription) {
|
||||||
this.conversionsSubscription.unsubscribe();
|
this.conversionsSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.processing = true;
|
||||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||||
async (conversions) => {
|
async (conversions) => {
|
||||||
this.conversions = conversions;
|
this.conversions = conversions;
|
||||||
@ -496,6 +504,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
console.error(`Unable to find apple pay button id='apple-pay-button'`);
|
console.error(`Unable to find apple pay button id='apple-pay-button'`);
|
||||||
// Try again
|
// Try again
|
||||||
setTimeout(this.requestApplePayPayment.bind(this), 500);
|
setTimeout(this.requestApplePayPayment.bind(this), 500);
|
||||||
|
this.processing = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.loadingApplePay = false;
|
this.loadingApplePay = false;
|
||||||
@ -507,6 +516,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||||
console.error(`Cannot retreive payment card details`);
|
console.error(`Cannot retreive payment card details`);
|
||||||
this.accelerateError = 'apple_pay_no_card_details';
|
this.accelerateError = 'apple_pay_no_card_details';
|
||||||
|
this.processing = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
||||||
@ -518,6 +528,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.accelerationUUID
|
this.accelerationUUID
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
|
this.processing = false;
|
||||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
if (this.applePay) {
|
if (this.applePay) {
|
||||||
@ -528,6 +539,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
error: (response) => {
|
error: (response) => {
|
||||||
|
this.processing = false;
|
||||||
this.accelerateError = response.error;
|
this.accelerateError = response.error;
|
||||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -539,6 +551,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
this.processing = false;
|
||||||
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
||||||
if (tokenResult.errors) {
|
if (tokenResult.errors) {
|
||||||
errorMessage += ` and errors: ${JSON.stringify(
|
errorMessage += ` and errors: ${JSON.stringify(
|
||||||
@ -549,6 +562,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
this.processing = false;
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -559,10 +573,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
* GOOGLE PAY
|
* GOOGLE PAY
|
||||||
*/
|
*/
|
||||||
async requestGooglePayPayment(): Promise<void> {
|
async requestGooglePayPayment(): Promise<void> {
|
||||||
|
if (this.processing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.conversionsSubscription) {
|
if (this.conversionsSubscription) {
|
||||||
this.conversionsSubscription.unsubscribe();
|
this.conversionsSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.processing = true;
|
||||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||||
async (conversions) => {
|
async (conversions) => {
|
||||||
this.conversions = conversions;
|
this.conversions = conversions;
|
||||||
@ -597,6 +615,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||||
console.error(`Cannot retreive payment card details`);
|
console.error(`Cannot retreive payment card details`);
|
||||||
this.accelerateError = 'apple_pay_no_card_details';
|
this.accelerateError = 'apple_pay_no_card_details';
|
||||||
|
this.processing = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
||||||
@ -608,6 +627,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.accelerationUUID
|
this.accelerationUUID
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
|
this.processing = false;
|
||||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
if (this.googlePay) {
|
if (this.googlePay) {
|
||||||
@ -618,6 +638,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
error: (response) => {
|
error: (response) => {
|
||||||
|
this.processing = false;
|
||||||
this.accelerateError = response.error;
|
this.accelerateError = response.error;
|
||||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -629,6 +650,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
this.processing = false;
|
||||||
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
||||||
if (tokenResult.errors) {
|
if (tokenResult.errors) {
|
||||||
errorMessage += ` and errors: ${JSON.stringify(
|
errorMessage += ` and errors: ${JSON.stringify(
|
||||||
@ -646,10 +668,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
* CASHAPP
|
* CASHAPP
|
||||||
*/
|
*/
|
||||||
async requestCashAppPayment(): Promise<void> {
|
async requestCashAppPayment(): Promise<void> {
|
||||||
|
if (this.processing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.conversionsSubscription) {
|
if (this.conversionsSubscription) {
|
||||||
this.conversionsSubscription.unsubscribe();
|
this.conversionsSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.processing = true;
|
||||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||||
async (conversions) => {
|
async (conversions) => {
|
||||||
this.conversions = conversions;
|
this.conversions = conversions;
|
||||||
@ -680,6 +706,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.cashAppPay.addEventListener('ontokenization', event => {
|
this.cashAppPay.addEventListener('ontokenization', event => {
|
||||||
const { tokenResult, error } = event.detail;
|
const { tokenResult, error } = event.detail;
|
||||||
if (error) {
|
if (error) {
|
||||||
|
this.processing = false;
|
||||||
this.accelerateError = error;
|
this.accelerateError = error;
|
||||||
} else if (tokenResult.status === 'OK') {
|
} else if (tokenResult.status === 'OK') {
|
||||||
this.servicesApiService.accelerateWithCashApp$(
|
this.servicesApiService.accelerateWithCashApp$(
|
||||||
@ -690,6 +717,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.accelerationUUID
|
this.accelerationUUID
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
|
this.processing = false;
|
||||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
if (this.cashAppPay) {
|
if (this.cashAppPay) {
|
||||||
@ -704,6 +732,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
error: (response) => {
|
error: (response) => {
|
||||||
|
this.processing = false;
|
||||||
this.accelerateError = response.error;
|
this.accelerateError = response.error;
|
||||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -47,13 +47,14 @@
|
|||||||
<tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()">
|
<tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()">
|
||||||
<td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
|
<td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
|
||||||
<td class="value" *ngIf="accelerationInfo.pools">
|
<td class="value" *ngIf="accelerationInfo.pools">
|
||||||
<ng-container *ngFor="let pool of accelerationInfo.pools">
|
<ng-container *ngFor="let pool of accelerationInfo.pools; let i = index;">
|
||||||
<img *ngIf="accelerationInfo.poolsData[pool]"
|
<img *ngIf="accelerationInfo.poolsData[pool]"
|
||||||
class="pool-logo"
|
class="pool-logo"
|
||||||
[style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'"
|
[style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'"
|
||||||
[src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'"
|
[src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'"
|
||||||
onError="this.src = '/resources/mining-pools/default.svg'"
|
onError="this.src = '/resources/mining-pools/default.svg'"
|
||||||
[alt]="'Logo of ' + pool.name + ' mining pool'">
|
[alt]="'Logo of ' + pool.name + ' mining pool'">
|
||||||
|
<br *ngIf="i % 6 === 5">
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
.label {
|
.label {
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pool-logo {
|
.pool-logo {
|
||||||
@ -30,7 +31,8 @@
|
|||||||
height: 22px;
|
height: 22px;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -1px;
|
top: -1px;
|
||||||
margin-right: 3px;
|
margin-right: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oobFees {
|
.oobFees {
|
||||||
|
@ -94,6 +94,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="(stateService.backend$ | async) === 'esplora' && address && utxos && utxos.length > 2">
|
||||||
|
<br>
|
||||||
|
<div class="title-tx">
|
||||||
|
<h2 class="text-left" i18n="address.unspent-outputs">Unspent Outputs</h2>
|
||||||
|
</div>
|
||||||
|
<div class="box">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md">
|
||||||
|
<app-utxo-graph [utxos]="utxos" left="80" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
<div class="title-tx">
|
<div class="title-tx">
|
||||||
<h2 class="text-left">
|
<h2 class="text-left">
|
||||||
|
@ -2,12 +2,12 @@ import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
|
|||||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||||
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
|
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
|
||||||
import { Address, ChainStats, Transaction, Vin } from '../../interfaces/electrs.interface';
|
import { Address, ChainStats, Transaction, Utxo, Vin } from '../../interfaces/electrs.interface';
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { AudioService } from '../../services/audio.service';
|
import { AudioService } from '../../services/audio.service';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { of, merge, Subscription, Observable } from 'rxjs';
|
import { of, merge, Subscription, Observable, forkJoin } from 'rxjs';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||||
import { AddressInformation } from '../../interfaces/node-api.interface';
|
import { AddressInformation } from '../../interfaces/node-api.interface';
|
||||||
@ -104,6 +104,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
addressString: string;
|
addressString: string;
|
||||||
isLoadingAddress = true;
|
isLoadingAddress = true;
|
||||||
transactions: Transaction[];
|
transactions: Transaction[];
|
||||||
|
utxos: Utxo[];
|
||||||
isLoadingTransactions = true;
|
isLoadingTransactions = true;
|
||||||
retryLoadMore = false;
|
retryLoadMore = false;
|
||||||
error: any;
|
error: any;
|
||||||
@ -159,6 +160,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
this.address = null;
|
this.address = null;
|
||||||
this.isLoadingTransactions = true;
|
this.isLoadingTransactions = true;
|
||||||
this.transactions = null;
|
this.transactions = null;
|
||||||
|
this.utxos = null;
|
||||||
this.addressInfo = null;
|
this.addressInfo = null;
|
||||||
this.exampleChannel = null;
|
this.exampleChannel = null;
|
||||||
document.body.scrollTo(0, 0);
|
document.body.scrollTo(0, 0);
|
||||||
@ -212,11 +214,19 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
this.updateChainStats();
|
this.updateChainStats();
|
||||||
this.isLoadingAddress = false;
|
this.isLoadingAddress = false;
|
||||||
this.isLoadingTransactions = true;
|
this.isLoadingTransactions = true;
|
||||||
return address.is_pubkey
|
const utxoCount = this.chainStats.utxos + this.mempoolStats.utxos;
|
||||||
|
return forkJoin([
|
||||||
|
address.is_pubkey
|
||||||
? this.electrsApiService.getScriptHashTransactions$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
|
? this.electrsApiService.getScriptHashTransactions$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
|
||||||
: this.electrsApiService.getAddressTransactions$(address.address);
|
: this.electrsApiService.getAddressTransactions$(address.address),
|
||||||
|
utxoCount >= 2 && utxoCount <= 500 ? (address.is_pubkey
|
||||||
|
? this.electrsApiService.getScriptHashUtxos$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
|
||||||
|
: this.electrsApiService.getAddressUtxos$(address.address)) : of([])
|
||||||
|
]);
|
||||||
}),
|
}),
|
||||||
switchMap((transactions) => {
|
switchMap(([transactions, utxos]) => {
|
||||||
|
this.utxos = utxos;
|
||||||
|
|
||||||
this.tempTransactions = transactions;
|
this.tempTransactions = transactions;
|
||||||
if (transactions.length) {
|
if (transactions.length) {
|
||||||
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
|
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
|
||||||
@ -334,6 +344,23 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update utxos in-place
|
||||||
|
for (const vin of transaction.vin) {
|
||||||
|
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout);
|
||||||
|
if (utxoIndex !== -1) {
|
||||||
|
this.utxos.splice(utxoIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [index, vout] of transaction.vout.entries()) {
|
||||||
|
if (vout.scriptpubkey_address === this.address.address) {
|
||||||
|
this.utxos.push({
|
||||||
|
txid: transaction.txid,
|
||||||
|
vout: index,
|
||||||
|
value: vout.value,
|
||||||
|
status: JSON.parse(JSON.stringify(transaction.status)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -346,6 +373,26 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
this.transactions.splice(index, 1);
|
this.transactions.splice(index, 1);
|
||||||
this.transactions = this.transactions.slice();
|
this.transactions = this.transactions.slice();
|
||||||
|
|
||||||
|
// update utxos in-place
|
||||||
|
for (const vin of transaction.vin) {
|
||||||
|
if (vin.prevout?.scriptpubkey_address === this.address.address) {
|
||||||
|
this.utxos.push({
|
||||||
|
txid: vin.txid,
|
||||||
|
vout: vin.vout,
|
||||||
|
value: vin.prevout.value,
|
||||||
|
status: { confirmed: true }, // Assuming the input was confirmed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [index, vout] of transaction.vout.entries()) {
|
||||||
|
if (vout.scriptpubkey_address === this.address.address) {
|
||||||
|
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index);
|
||||||
|
if (utxoIndex !== -1) {
|
||||||
|
this.utxos.splice(utxoIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<span class="input-group-text">{{ currency$ | async }}</span>
|
<span class="input-group-text">{{ currency$ | async }}</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" class="form-control" formControlName="fiat" (input)="transformInput('fiat')" (click)="selectAll($event)">
|
<input type="text" inputmode="numeric" class="form-control" formControlName="fiat" (input)="transformInput('fiat')" (click)="selectAll($event)">
|
||||||
<app-clipboard [button]="true" [text]="form.get('fiat').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
|
<app-clipboard [button]="true" [text]="form.get('fiat').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -20,7 +20,7 @@
|
|||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<span class="input-group-text">BTC</span>
|
<span class="input-group-text">BTC</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" class="form-control" formControlName="bitcoin" (input)="transformInput('bitcoin')" (click)="selectAll($event)">
|
<input type="text" inputmode="numeric" class="form-control" formControlName="bitcoin" (input)="transformInput('bitcoin')" (click)="selectAll($event)">
|
||||||
<app-clipboard [button]="true" [text]="form.get('bitcoin').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
|
<app-clipboard [button]="true" [text]="form.get('bitcoin').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -28,7 +28,7 @@
|
|||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<span class="input-group-text" i18n="shared.sats">sats</span>
|
<span class="input-group-text" i18n="shared.sats">sats</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" class="form-control" formControlName="satoshis" (input)="transformInput('satoshis')" (click)="selectAll($event)">
|
<input type="text" inputmode="numeric" class="form-control" formControlName="satoshis" (input)="transformInput('satoshis')" (click)="selectAll($event)">
|
||||||
<app-clipboard [button]="true" [text]="form.get('satoshis').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
|
<app-clipboard [button]="true" [text]="form.get('satoshis').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -65,23 +65,25 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field narrower">
|
@if (!replaced) {
|
||||||
<div class="label" i18n="transaction.eta|Transaction ETA">ETA</div>
|
<div class="field narrower">
|
||||||
<div class="value">
|
<div class="label" i18n="transaction.eta|Transaction ETA">ETA</div>
|
||||||
<ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton">
|
<div class="value">
|
||||||
<span class="justify-content-end d-flex align-items-center">
|
<ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton">
|
||||||
@if (eta.blocks >= 7) {
|
<span class="justify-content-end d-flex align-items-center">
|
||||||
<span i18n="transaction.eta.not-any-time-soon|Transaction ETA mot any time soon">Not any time soon</span>
|
@if (eta.blocks >= 7) {
|
||||||
} @else {
|
<span i18n="transaction.eta.not-any-time-soon|Transaction ETA mot any time soon">Not any time soon</span>
|
||||||
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
|
} @else {
|
||||||
}
|
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||||
</span>
|
}
|
||||||
</ng-container>
|
</span>
|
||||||
<ng-template #etaSkeleton>
|
</ng-container>
|
||||||
<span class="skeleton-loader" style="max-width: 200px;"></span>
|
<ng-template #etaSkeleton>
|
||||||
</ng-template>
|
<span class="skeleton-loader" style="max-width: 200px;"></span>
|
||||||
</div>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
} @else if (tx && tx.status?.confirmed) {
|
} @else if (tx && tx.status?.confirmed) {
|
||||||
<div class="field narrower mt-2">
|
<div class="field narrower mt-2">
|
||||||
<div class="label" i18n="transaction.confirmed-at">Confirmed at</div>
|
<div class="label" i18n="transaction.confirmed-at">Confirmed at</div>
|
||||||
|
@ -192,7 +192,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.hideAccelerationSummary = this.stateService.isMempoolSpaceBuild ? this.storageService.getValue('hide-accelerator-pref') == 'true' : true;
|
this.hideAccelerationSummary = this.stateService.isMempoolSpaceBuild ? this.storageService.getValue('hide-accelerator-pref') == 'true' : true;
|
||||||
|
|
||||||
if (!this.stateService.isLiquid()) {
|
if (!this.stateService.isLiquid()) {
|
||||||
this.miningService.getMiningStats('1w').subscribe(stats => {
|
this.miningService.getMiningStats('1m').subscribe(stats => {
|
||||||
this.miningStats = stats;
|
this.miningStats = stats;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -491,7 +491,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
if (this.stateService.network === '') {
|
if (this.stateService.network === '') {
|
||||||
if (!this.mempoolPosition.accelerated) {
|
if (!this.mempoolPosition.accelerated) {
|
||||||
if (!this.accelerationFlowCompleted && !this.hideAccelerationSummary && !this.showAccelerationSummary) {
|
if (!this.accelerationFlowCompleted && !this.hideAccelerationSummary && !this.showAccelerationSummary) {
|
||||||
this.miningService.getMiningStats('1w').subscribe(stats => {
|
this.miningService.getMiningStats('1m').subscribe(stats => {
|
||||||
this.miningStats = stats;
|
this.miningStats = stats;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
|
||||||
|
|
||||||
|
<div [class.full-container]="!widget">
|
||||||
|
<ng-container *ngIf="!error">
|
||||||
|
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null, paddingBottom: !widget}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||||
|
(chartInit)="onChartInit($event)">
|
||||||
|
</div>
|
||||||
|
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||||
|
<div class="spinner-border text-light"></div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="error">
|
||||||
|
<div class="error-wrapper">
|
||||||
|
<p class="error">{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||||
|
<div class="spinner-border text-light"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,59 @@
|
|||||||
|
.card-header {
|
||||||
|
border-bottom: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
@media (min-width: 465px) {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-title {
|
||||||
|
position: relative;
|
||||||
|
color: var(--fg);
|
||||||
|
opacity: var(--opacity);
|
||||||
|
margin-top: -13px;
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
padding-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0px;
|
||||||
|
width: 100%;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
font-size: 15px;
|
||||||
|
color: grey;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
.chart-widget {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
285
frontend/src/app/components/utxo-graph/utxo-graph.component.ts
Normal file
285
frontend/src/app/components/utxo-graph/utxo-graph.component.ts
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, NgZone, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
|
||||||
|
import { EChartsOption } from '../../graphs/echarts';
|
||||||
|
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||||
|
import { Utxo } from '../../interfaces/electrs.interface';
|
||||||
|
import { StateService } from '../../services/state.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||||
|
import { renderSats } from '../../shared/common.utils';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-utxo-graph',
|
||||||
|
templateUrl: './utxo-graph.component.html',
|
||||||
|
styleUrls: ['./utxo-graph.component.scss'],
|
||||||
|
styles: [`
|
||||||
|
.loadingGraphs {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: calc(50% - 15px);
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
`],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class UtxoGraphComponent implements OnChanges, OnDestroy {
|
||||||
|
@Input() utxos: Utxo[];
|
||||||
|
@Input() height: number = 200;
|
||||||
|
@Input() right: number | string = 10;
|
||||||
|
@Input() left: number | string = 70;
|
||||||
|
@Input() widget: boolean = false;
|
||||||
|
|
||||||
|
subscription: Subscription;
|
||||||
|
redraw$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||||
|
|
||||||
|
chartOptions: EChartsOption = {};
|
||||||
|
chartInitOptions = {
|
||||||
|
renderer: 'svg',
|
||||||
|
};
|
||||||
|
|
||||||
|
error: any;
|
||||||
|
isLoading = true;
|
||||||
|
chartInstance: any = undefined;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public stateService: StateService,
|
||||||
|
private cd: ChangeDetectorRef,
|
||||||
|
private zone: NgZone,
|
||||||
|
private router: Router,
|
||||||
|
private relativeUrlPipe: RelativeUrlPipe,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
this.isLoading = true;
|
||||||
|
if (!this.utxos) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (changes.utxos) {
|
||||||
|
this.prepareChartOptions(this.utxos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareChartOptions(utxos: Utxo[]) {
|
||||||
|
if (!utxos || utxos.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
const distance = (x1: number, y1: number, x2: number, y2: number): number => Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
||||||
|
const intersectionPoints = (x1: number, y1: number, r1: number, x2: number, y2: number, r2: number): [number, number][] => {
|
||||||
|
const d = distance(x1, y1, x2, y2);
|
||||||
|
const a = (r1 * r1 - r2 * r2 + d * d) / (2 * d);
|
||||||
|
const h = Math.sqrt(r1 * r1 - a * a);
|
||||||
|
const x3 = x1 + a * (x2 - x1) / d;
|
||||||
|
const y3 = y1 + a * (y2 - y1) / d;
|
||||||
|
return [
|
||||||
|
[x3 + h * (y2 - y1) / d, y3 - h * (x2 - x1) / d],
|
||||||
|
[x3 - h * (y2 - y1) / d, y3 + h * (x2 - x1) / d]
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Naive algorithm to pack circles as tightly as possible without overlaps
|
||||||
|
const placedCircles: { x: number, y: number, r: number, utxo: Utxo, distances: number[] }[] = [];
|
||||||
|
// Pack in descending order of value, and limit to the top 500 to preserve performance
|
||||||
|
const sortedUtxos = utxos.sort((a, b) => b.value - a.value).slice(0, 500);
|
||||||
|
let centerOfMass = { x: 0, y: 0 };
|
||||||
|
let weightOfMass = 0;
|
||||||
|
sortedUtxos.forEach((utxo, index) => {
|
||||||
|
// area proportional to value
|
||||||
|
const r = Math.sqrt(utxo.value);
|
||||||
|
|
||||||
|
// special cases for the first two utxos
|
||||||
|
if (index === 0) {
|
||||||
|
placedCircles.push({ x: 0, y: 0, r, utxo, distances: [0] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (index === 1) {
|
||||||
|
const c = placedCircles[0];
|
||||||
|
placedCircles.push({ x: c.r + r, y: 0, r, utxo, distances: [c.r + r, 0] });
|
||||||
|
c.distances.push(c.r + r);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The best position will be touching two other circles
|
||||||
|
// generate a list of candidate points by finding all such positions
|
||||||
|
// where the circle can be placed without overlapping other circles
|
||||||
|
const candidates: [number, number, number[]][] = [];
|
||||||
|
const numCircles = placedCircles.length;
|
||||||
|
for (let i = 0; i < numCircles; i++) {
|
||||||
|
for (let j = i + 1; j < numCircles; j++) {
|
||||||
|
const c1 = placedCircles[i];
|
||||||
|
const c2 = placedCircles[j];
|
||||||
|
if (c1.distances[j] > (c1.r + c2.r + r + r)) {
|
||||||
|
// too far apart for new circle to touch both
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const points = intersectionPoints(c1.x, c1.y, c1.r + r, c2.x, c2.y, c2.r + r);
|
||||||
|
points.forEach(([x, y]) => {
|
||||||
|
const distances: number[] = [];
|
||||||
|
let valid = true;
|
||||||
|
for (let k = 0; k < numCircles; k++) {
|
||||||
|
const c = placedCircles[k];
|
||||||
|
const d = distance(x, y, c.x, c.y);
|
||||||
|
if (k !== i && k !== j && d < (r + c.r)) {
|
||||||
|
valid = false;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
distances.push(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (valid) {
|
||||||
|
candidates.push([x, y, distances]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick the candidate closest to the center of mass
|
||||||
|
const [x, y, distances] = candidates.length ? candidates.reduce((closest, candidate) =>
|
||||||
|
distance(candidate[0], candidate[1], centerOfMass[0], centerOfMass[1]) <
|
||||||
|
distance(closest[0], closest[1], centerOfMass[0], centerOfMass[1])
|
||||||
|
? candidate
|
||||||
|
: closest
|
||||||
|
) : [0, 0, []];
|
||||||
|
|
||||||
|
placedCircles.push({ x, y, r, utxo, distances });
|
||||||
|
for (let i = 0; i < distances.length; i++) {
|
||||||
|
placedCircles[i].distances.push(distances[i]);
|
||||||
|
}
|
||||||
|
distances.push(0);
|
||||||
|
|
||||||
|
// Update center of mass
|
||||||
|
centerOfMass = {
|
||||||
|
x: (centerOfMass.x * weightOfMass + x) / (weightOfMass + r),
|
||||||
|
y: (centerOfMass.y * weightOfMass + y) / (weightOfMass + r),
|
||||||
|
};
|
||||||
|
weightOfMass += r;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Precompute the bounding box of the graph
|
||||||
|
const minX = Math.min(...placedCircles.map(d => d.x - d.r));
|
||||||
|
const maxX = Math.max(...placedCircles.map(d => d.x + d.r));
|
||||||
|
const minY = Math.min(...placedCircles.map(d => d.y - d.r));
|
||||||
|
const maxY = Math.max(...placedCircles.map(d => d.y + d.r));
|
||||||
|
const width = maxX - minX;
|
||||||
|
const height = maxY - minY;
|
||||||
|
|
||||||
|
const data = placedCircles.map((circle, index) => [
|
||||||
|
circle.utxo,
|
||||||
|
index,
|
||||||
|
circle.x,
|
||||||
|
circle.y,
|
||||||
|
circle.r
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.chartOptions = {
|
||||||
|
series: [{
|
||||||
|
type: 'custom',
|
||||||
|
coordinateSystem: undefined,
|
||||||
|
data,
|
||||||
|
renderItem: (params, api) => {
|
||||||
|
const idx = params.dataIndex;
|
||||||
|
const datum = data[idx];
|
||||||
|
const utxo = datum[0] as Utxo;
|
||||||
|
const chartWidth = api.getWidth();
|
||||||
|
const chartHeight = api.getHeight();
|
||||||
|
const scale = Math.min(chartWidth / width, chartHeight / height);
|
||||||
|
const scaledWidth = width * scale;
|
||||||
|
const scaledHeight = height * scale;
|
||||||
|
const offsetX = (chartWidth - scaledWidth) / 2 - minX * scale;
|
||||||
|
const offsetY = (chartHeight - scaledHeight) / 2 - minY * scale;
|
||||||
|
const x = datum[2] as number;
|
||||||
|
const y = datum[3] as number;
|
||||||
|
const r = datum[4] as number;
|
||||||
|
if (r * scale < 3) {
|
||||||
|
// skip items too small to render cleanly
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const valueStr = renderSats(utxo.value, this.stateService.network);
|
||||||
|
const elements: any[] = [
|
||||||
|
{
|
||||||
|
type: 'circle',
|
||||||
|
autoBatch: true,
|
||||||
|
shape: {
|
||||||
|
cx: (x * scale) + offsetX,
|
||||||
|
cy: (y * scale) + offsetY,
|
||||||
|
r: (r * scale) - 1,
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
fill: '#5470c6',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const labelFontSize = Math.min(36, r * scale * 0.25);
|
||||||
|
if (labelFontSize > 8) {
|
||||||
|
elements.push({
|
||||||
|
type: 'text',
|
||||||
|
x: (x * scale) + offsetX,
|
||||||
|
y: (y * scale) + offsetY,
|
||||||
|
style: {
|
||||||
|
text: valueStr,
|
||||||
|
fontSize: labelFontSize,
|
||||||
|
fill: '#fff',
|
||||||
|
align: 'center',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'group',
|
||||||
|
children: elements,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||||
|
borderRadius: 4,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
textStyle: {
|
||||||
|
color: 'var(--tooltip-grey)',
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
borderColor: '#000',
|
||||||
|
formatter: (params: any): string => {
|
||||||
|
const utxo = params.data[0] as Utxo;
|
||||||
|
const valueStr = renderSats(utxo.value, this.stateService.network);
|
||||||
|
return `
|
||||||
|
<b style="color: white;">${utxo.txid.slice(0, 6)}...${utxo.txid.slice(-6)}:${utxo.vout}</b>
|
||||||
|
<br>
|
||||||
|
${valueStr}`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
onChartClick(e): void {
|
||||||
|
if (e.data?.[0]?.txid) {
|
||||||
|
this.zone.run(() => {
|
||||||
|
const url = this.relativeUrlPipe.transform(`/tx/${e.data[0].txid}`);
|
||||||
|
if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) {
|
||||||
|
window.open(url + '?mode=details#vout=' + e.data[0].vout);
|
||||||
|
} else {
|
||||||
|
this.router.navigate([url], { fragment: `vout=${e.data[0].vout}` });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChartInit(ec): void {
|
||||||
|
this.chartInstance = ec;
|
||||||
|
this.chartInstance.on('click', 'series', this.onChartClick.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.subscription) {
|
||||||
|
this.subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isMobile(): boolean {
|
||||||
|
return (window.innerWidth <= 767.98);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
// Import tree-shakeable echarts
|
// Import tree-shakeable echarts
|
||||||
import * as echarts from 'echarts/core';
|
import * as echarts from 'echarts/core';
|
||||||
import { LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart } from 'echarts/charts';
|
import { LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart, CustomChart } from 'echarts/charts';
|
||||||
import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, GeoComponent, DataZoomComponent, VisualMapComponent, MarkLineComponent } from 'echarts/components';
|
import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, GeoComponent, DataZoomComponent, VisualMapComponent, MarkLineComponent } from 'echarts/components';
|
||||||
import { SVGRenderer, CanvasRenderer } from 'echarts/renderers';
|
import { SVGRenderer, CanvasRenderer } from 'echarts/renderers';
|
||||||
// Typescript interfaces
|
// Typescript interfaces
|
||||||
@ -12,6 +12,7 @@ echarts.use([
|
|||||||
TitleComponent, TooltipComponent, GridComponent,
|
TitleComponent, TooltipComponent, GridComponent,
|
||||||
LegendComponent, GeoComponent, DataZoomComponent,
|
LegendComponent, GeoComponent, DataZoomComponent,
|
||||||
VisualMapComponent, MarkLineComponent,
|
VisualMapComponent, MarkLineComponent,
|
||||||
LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart
|
LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart,
|
||||||
|
CustomChart,
|
||||||
]);
|
]);
|
||||||
export { echarts, EChartsOption, TreemapSeriesOption, LineSeriesOption, PieSeriesOption };
|
export { echarts, EChartsOption, TreemapSeriesOption, LineSeriesOption, PieSeriesOption };
|
@ -36,6 +36,7 @@ import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools
|
|||||||
import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component';
|
import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component';
|
||||||
import { AddressComponent } from '../components/address/address.component';
|
import { AddressComponent } from '../components/address/address.component';
|
||||||
import { AddressGraphComponent } from '../components/address-graph/address-graph.component';
|
import { AddressGraphComponent } from '../components/address-graph/address-graph.component';
|
||||||
|
import { UtxoGraphComponent } from '../components/utxo-graph/utxo-graph.component';
|
||||||
import { ActiveAccelerationBox } from '../components/acceleration/active-acceleration-box/active-acceleration-box.component';
|
import { ActiveAccelerationBox } from '../components/acceleration/active-acceleration-box/active-acceleration-box.component';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
@ -76,6 +77,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
HashrateChartPoolsComponent,
|
HashrateChartPoolsComponent,
|
||||||
BlockHealthGraphComponent,
|
BlockHealthGraphComponent,
|
||||||
AddressGraphComponent,
|
AddressGraphComponent,
|
||||||
|
UtxoGraphComponent,
|
||||||
ActiveAccelerationBox,
|
ActiveAccelerationBox,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -233,3 +233,10 @@ interface AssetStats {
|
|||||||
peg_out_amount: number;
|
peg_out_amount: number;
|
||||||
burn_count: number;
|
burn_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Utxo {
|
||||||
|
txid: string;
|
||||||
|
vout: number;
|
||||||
|
value: number;
|
||||||
|
status: Status;
|
||||||
|
}
|
@ -13,7 +13,8 @@ class GuardService {
|
|||||||
|
|
||||||
trackerGuard(route: Route, segments: UrlSegment[]): boolean {
|
trackerGuard(route: Route, segments: UrlSegment[]): boolean {
|
||||||
const preferredRoute = this.router.getCurrentNavigation()?.extractedUrl.queryParams?.mode;
|
const preferredRoute = this.router.getCurrentNavigation()?.extractedUrl.queryParams?.mode;
|
||||||
return (preferredRoute === 'status' || (preferredRoute !== 'details' && this.navigationService.isInitialLoad())) && window.innerWidth <= 767.98;
|
const path = this.router.getCurrentNavigation()?.extractedUrl.root.children.primary.segments;
|
||||||
|
return (preferredRoute === 'status' || (preferredRoute !== 'details' && this.navigationService.isInitialLoad())) && window.innerWidth <= 767.98 && !(path.length === 2 && ['push', 'test'].includes(path[1].path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { BehaviorSubject, Observable, catchError, filter, from, of, shareReplay, switchMap, take, tap } from 'rxjs';
|
import { BehaviorSubject, Observable, catchError, filter, from, of, shareReplay, switchMap, take, tap } from 'rxjs';
|
||||||
import { Transaction, Address, Outspend, Recent, Asset, ScriptHash, AddressTxSummary } from '../interfaces/electrs.interface';
|
import { Transaction, Address, Outspend, Recent, Asset, ScriptHash, AddressTxSummary, Utxo } from '../interfaces/electrs.interface';
|
||||||
import { StateService } from './state.service';
|
import { StateService } from './state.service';
|
||||||
import { BlockExtended } from '../interfaces/node-api.interface';
|
import { BlockExtended } from '../interfaces/node-api.interface';
|
||||||
import { calcScriptHash$ } from '../bitcoin.utils';
|
import { calcScriptHash$ } from '../bitcoin.utils';
|
||||||
@ -166,6 +166,16 @@ export class ElectrsApiService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAddressUtxos$(address: string): Observable<Utxo[]> {
|
||||||
|
return this.httpClient.get<Utxo[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/utxo');
|
||||||
|
}
|
||||||
|
|
||||||
|
getScriptHashUtxos$(script: string): Observable<Utxo[]> {
|
||||||
|
return from(calcScriptHash$(script)).pipe(
|
||||||
|
switchMap(scriptHash => this.httpClient.get<Utxo[]>(this.apiBaseUrl + this.apiBasePath + '/api/scripthash/' + scriptHash + '/utxo')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
getAsset$(assetId: string): Observable<Asset> {
|
getAsset$(assetId: string): Observable<Asset> {
|
||||||
return this.httpClient.get<Asset>(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId);
|
return this.httpClient.get<Asset>(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId);
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ export class EtaService {
|
|||||||
return combineLatest([
|
return combineLatest([
|
||||||
this.stateService.mempoolTxPosition$.pipe(map(p => p?.position)),
|
this.stateService.mempoolTxPosition$.pipe(map(p => p?.position)),
|
||||||
this.stateService.difficultyAdjustment$,
|
this.stateService.difficultyAdjustment$,
|
||||||
miningStats ? of(miningStats) : this.miningService.getMiningStats('1w'),
|
miningStats ? of(miningStats) : this.miningService.getMiningStats('1m'),
|
||||||
]).pipe(
|
]).pipe(
|
||||||
map(([mempoolPosition, da, miningStats]) => {
|
map(([mempoolPosition, da, miningStats]) => {
|
||||||
if (!mempoolPosition || !estimate?.pools?.length || !miningStats || !da) {
|
if (!mempoolPosition || !estimate?.pools?.length || !miningStats || !da) {
|
||||||
@ -166,7 +166,7 @@ export class EtaService {
|
|||||||
pools[pool.poolUniqueId] = pool;
|
pools[pool.poolUniqueId] = pool;
|
||||||
}
|
}
|
||||||
const unacceleratedPosition = this.mempoolPositionFromFees(getUnacceleratedFeeRate(tx, true), mempoolBlocks);
|
const unacceleratedPosition = this.mempoolPositionFromFees(getUnacceleratedFeeRate(tx, true), mempoolBlocks);
|
||||||
const totalAcceleratedHashrate = accelerationPositions.reduce((total, pos) => total + (pools[pos.poolId].lastEstimatedHashrate), 0);
|
const totalAcceleratedHashrate = accelerationPositions.reduce((total, pos) => total + (pools[pos.poolId]?.lastEstimatedHashrate || 0), 0);
|
||||||
const shares = [
|
const shares = [
|
||||||
{
|
{
|
||||||
block: unacceleratedPosition.block,
|
block: unacceleratedPosition.block,
|
||||||
@ -174,7 +174,7 @@ export class EtaService {
|
|||||||
},
|
},
|
||||||
...accelerationPositions.map(pos => ({
|
...accelerationPositions.map(pos => ({
|
||||||
block: pos.block,
|
block: pos.block,
|
||||||
hashrateShare: ((pools[pos.poolId].lastEstimatedHashrate) / miningStats.lastEstimatedHashrate)
|
hashrateShare: ((pools[pos.poolId]?.lastEstimatedHashrate || 0) / miningStats.lastEstimatedHashrate)
|
||||||
}))
|
}))
|
||||||
];
|
];
|
||||||
return this.calculateETAFromShares(shares, da);
|
return this.calculateETAFromShares(shares, da);
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { MempoolBlockDelta, MempoolBlockDeltaCompressed, MempoolDeltaChange, TransactionCompressed } from "../interfaces/websocket.interface";
|
import { MempoolBlockDelta, MempoolBlockDeltaCompressed, MempoolDeltaChange, TransactionCompressed } from "../interfaces/websocket.interface";
|
||||||
import { TransactionStripped } from "../interfaces/node-api.interface";
|
import { TransactionStripped } from "../interfaces/node-api.interface";
|
||||||
|
import { AmountShortenerPipe } from "./pipes/amount-shortener.pipe";
|
||||||
|
const amountShortenerPipe = new AmountShortenerPipe();
|
||||||
|
|
||||||
export function isMobile(): boolean {
|
export function isMobile(): boolean {
|
||||||
return (window.innerWidth <= 767.98);
|
return (window.innerWidth <= 767.98);
|
||||||
@ -184,6 +186,33 @@ export function uncompressDeltaChange(block: number, delta: MempoolBlockDeltaCom
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function renderSats(value: number, network: string, mode: 'sats' | 'btc' | 'auto' = 'auto'): string {
|
||||||
|
let prefix = '';
|
||||||
|
switch (network) {
|
||||||
|
case 'liquid':
|
||||||
|
prefix = 'L';
|
||||||
|
break;
|
||||||
|
case 'liquidtestnet':
|
||||||
|
prefix = 'tL';
|
||||||
|
break;
|
||||||
|
case 'testnet':
|
||||||
|
case 'testnet4':
|
||||||
|
prefix = 't';
|
||||||
|
break;
|
||||||
|
case 'signet':
|
||||||
|
prefix = 's';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (mode === 'btc' || (mode === 'auto' && value >= 1000000)) {
|
||||||
|
return `${amountShortenerPipe.transform(value / 100000000)} ${prefix}BTC`;
|
||||||
|
} else {
|
||||||
|
if (prefix.length) {
|
||||||
|
prefix += '-';
|
||||||
|
}
|
||||||
|
return `${amountShortenerPipe.transform(value)} ${prefix}sats`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function insecureRandomUUID(): string {
|
export function insecureRandomUUID(): string {
|
||||||
const hexDigits = '0123456789abcdef';
|
const hexDigits = '0123456789abcdef';
|
||||||
const uuidLengths = [8, 4, 4, 4, 12];
|
const uuidLengths = [8, 4, 4, 4, 12];
|
||||||
|
@ -13,8 +13,13 @@
|
|||||||
</div>
|
</div>
|
||||||
@if (!enterpriseInfo?.footer_img) {
|
@if (!enterpriseInfo?.footer_img) {
|
||||||
<p class="explore-tagline-mobile">
|
<p class="explore-tagline-mobile">
|
||||||
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
|
@if (officialMempoolSpace) {
|
||||||
<ng-template [ngIf]="locale.substr(0, 2) === 'en'">®</ng-template>
|
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
|
||||||
|
<ng-template [ngIf]="locale.substr(0, 2) === 'en'">®</ng-template>
|
||||||
|
} @else {
|
||||||
|
<ng-container i18n="shared.be-your-own-explorer">Be your own explorer</ng-container>
|
||||||
|
<ng-template [ngIf]="locale.substr(0, 2) === 'en'">™</ng-template>
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
<div class="site-options language-selector d-flex justify-content-center align-items-center" [class]="{'services': isServicesPage}">
|
<div class="site-options language-selector d-flex justify-content-center align-items-center" [class]="{'services': isServicesPage}">
|
||||||
@ -52,8 +57,13 @@
|
|||||||
<span *ngIf="!user" i18n="shared.sign-in" class="nowrap">Sign In</span>
|
<span *ngIf="!user" i18n="shared.sign-in" class="nowrap">Sign In</span>
|
||||||
</a>
|
</a>
|
||||||
<p class="explore-tagline-desktop">
|
<p class="explore-tagline-desktop">
|
||||||
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
|
@if (officialMempoolSpace) {
|
||||||
<ng-template [ngIf]="locale.substr(0, 2) === 'en'">®</ng-template>
|
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
|
||||||
|
<ng-template [ngIf]="locale.substr(0, 2) === 'en'">®</ng-template>
|
||||||
|
} @else {
|
||||||
|
<ng-container i18n="shared.be-your-own-explorer">Be your own explorer</ng-container>
|
||||||
|
<ng-template [ngIf]="locale.substr(0, 2) === 'en'">™</ng-template>
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user