Merge branch 'master' into natsoni/decode-tx
This commit is contained in:
@@ -439,4 +439,39 @@ export const fiatCurrencies = {
|
||||
code: 'ZAR',
|
||||
indexed: true,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export interface Timezone {
|
||||
offset: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const timezones: Timezone[] = [
|
||||
{ offset: '-12', name: 'Anywhere on Earth (AoE)' },
|
||||
{ offset: '-11', name: 'Samoa Standard Time (SST)' },
|
||||
{ offset: '-10', name: 'Hawaii Standard Time (HST)' },
|
||||
{ offset: '-9', name: 'Alaska Standard Time (AKST)' },
|
||||
{ offset: '-8', name: 'Pacific Standard Time (PST)' },
|
||||
{ offset: '-7', name: 'Mountain Standard Time (MST)' },
|
||||
{ offset: '-6', name: 'Central Standard Time (CST)' },
|
||||
{ offset: '-5', name: 'Eastern Standard Time (EST)' },
|
||||
{ offset: '-4', name: 'Atlantic Standard Time (AST)' },
|
||||
{ offset: '-3', name: 'Argentina Time (ART)' },
|
||||
{ offset: '-2', name: 'Fernando de Noronha Time (FNT)' },
|
||||
{ offset: '-1', name: 'Azores Time (AZOT)' },
|
||||
{ offset: '+0', name: 'Greenwich Mean Time (GMT)' },
|
||||
{ offset: '+1', name: 'Central European Time (CET)' },
|
||||
{ offset: '+2', name: 'Eastern European Time (EET)' },
|
||||
{ offset: '+3', name: 'Moscow Standard Time (MSK)' },
|
||||
{ offset: '+4', name: 'Armenia Time (AMT)' },
|
||||
{ offset: '+5', name: 'Pakistan Standard Time (PKT)' },
|
||||
{ offset: '+6', name: 'Xinjiang Time (XJT)' },
|
||||
{ offset: '+7', name: 'Indochina Time (ICT)' },
|
||||
{ offset: '+8', name: 'Hong Kong Time (HKT)' },
|
||||
{ offset: '+9', name: 'Japan Standard Time (JST)' },
|
||||
{ offset: '+10', name: 'Australian Eastern Standard Time (AEST)' },
|
||||
{ offset: '+11', name: 'Norfolk Time (NFT)' },
|
||||
{ offset: '+12', name: 'New Zealand Standard Time (NZST)' },
|
||||
{ offset: '+13', name: 'Tonga Time (TOT)' },
|
||||
{ offset: '+14', name: 'Line Islands Time (LINT)' }
|
||||
];
|
||||
@@ -612,10 +612,18 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
const verificationToken = await this.$verifyBuyer(this.payments, tokenResult.token, tokenResult.details, costUSD.toFixed(2));
|
||||
if (!verificationToken) {
|
||||
console.error(`SCA verification failed`);
|
||||
this.accelerateError = 'SCA Verification Failed. Payment Declined.';
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
||||
this.servicesApiService.accelerateWithGooglePay$(
|
||||
this.tx.txid,
|
||||
tokenResult.token,
|
||||
verificationToken,
|
||||
cardTag,
|
||||
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||
costUSD
|
||||
@@ -743,6 +751,32 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Required in SCA Mandated Regions: Learn more at https://developer.squareup.com/docs/sca-overview
|
||||
*/
|
||||
async $verifyBuyer(payments, token, details, amount) {
|
||||
const verificationDetails = {
|
||||
amount: amount,
|
||||
currencyCode: 'USD',
|
||||
intent: 'CHARGE',
|
||||
billingContact: {
|
||||
givenName: details.card?.billing?.givenName,
|
||||
familyName: details.card?.billing?.familyName,
|
||||
phone: details.card?.billing?.phone,
|
||||
addressLines: details.card?.billing?.addressLines,
|
||||
city: details.card?.billing?.city,
|
||||
state: details.card?.billing?.state,
|
||||
countryCode: details.card?.billing?.countryCode,
|
||||
},
|
||||
};
|
||||
|
||||
const verificationResults = await payments.verifyBuyer(
|
||||
token,
|
||||
verificationDetails,
|
||||
);
|
||||
return verificationResults.token;
|
||||
}
|
||||
|
||||
/**
|
||||
* BTCPay
|
||||
*/
|
||||
|
||||
@@ -45,14 +45,17 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
@Input() left: number | string = 70;
|
||||
@Input() widget: boolean = false;
|
||||
@Input() defaultFiat: boolean = false;
|
||||
@Input() showLegend: boolean = true;
|
||||
@Input() showYAxis: boolean = true;
|
||||
|
||||
adjustedLeft: number;
|
||||
adjustedRight: number;
|
||||
data: any[] = [];
|
||||
fiatData: any[] = [];
|
||||
hoverData: any[] = [];
|
||||
conversions: any;
|
||||
allowZoom: boolean = false;
|
||||
initialRight = this.right;
|
||||
initialLeft = this.left;
|
||||
|
||||
selected = { [$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`]: true, 'Fiat': false };
|
||||
|
||||
subscription: Subscription;
|
||||
@@ -120,7 +123,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
} else if (this.conversions && this.conversions['USD']) {
|
||||
price = this.conversions['USD'];
|
||||
}
|
||||
return { ...item, price: price }
|
||||
return { ...item, price: price };
|
||||
});
|
||||
}
|
||||
}),
|
||||
@@ -181,8 +184,8 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0);
|
||||
const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] ?? d.value[1])), maxValue);
|
||||
|
||||
this.right = this.selected['Fiat'] ? +this.initialRight + 40 : this.initialRight;
|
||||
this.left = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? this.initialLeft : +this.initialLeft - 40;
|
||||
this.adjustedRight = this.selected['Fiat'] ? +this.right + 40 : +this.right;
|
||||
this.adjustedLeft = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? +this.left : +this.left - 40;
|
||||
|
||||
this.chartOptions = {
|
||||
color: [
|
||||
@@ -199,10 +202,10 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
grid: {
|
||||
top: 20,
|
||||
bottom: this.allowZoom ? 65 : 20,
|
||||
right: this.right,
|
||||
left: this.left,
|
||||
right: this.adjustedRight,
|
||||
left: this.adjustedLeft,
|
||||
},
|
||||
legend: !this.stateService.isAnyTestnet() ? {
|
||||
legend: (this.showLegend && !this.stateService.isAnyTestnet()) ? {
|
||||
data: [
|
||||
{
|
||||
name: $localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`,
|
||||
@@ -313,6 +316,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
type: 'value',
|
||||
position: 'left',
|
||||
axisLabel: {
|
||||
show: this.showYAxis,
|
||||
color: 'rgb(110, 112, 121)',
|
||||
formatter: (val): string => {
|
||||
let valSpan = maxValue - (this.period === 'all' ? 0 : minValue);
|
||||
@@ -343,9 +347,10 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
{
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
show: this.showYAxis,
|
||||
color: 'rgb(110, 112, 121)',
|
||||
formatter: function(val) {
|
||||
return `$${this.amountShortenerPipe.transform(val, 0, undefined, true)}`;
|
||||
return `$${this.amountShortenerPipe.transform(val, 3, undefined, true, true)}`;
|
||||
}.bind(this)
|
||||
},
|
||||
splitLine: {
|
||||
@@ -399,8 +404,8 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
type: 'slider',
|
||||
brushSelect: false,
|
||||
realtime: true,
|
||||
left: this.left,
|
||||
right: this.right,
|
||||
left: this.adjustedLeft,
|
||||
right: this.adjustedRight,
|
||||
selectedDataBackground: {
|
||||
lineStyle: {
|
||||
color: '#fff',
|
||||
@@ -413,7 +418,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
|
||||
onChartClick(e) {
|
||||
if (this.hoverData?.length && this.hoverData[0]?.[2]?.txid) {
|
||||
this.zone.run(() => {
|
||||
this.zone.run(() => {
|
||||
const url = this.relativeUrlPipe.transform(`/tx/${this.hoverData[0][2].txid}`);
|
||||
if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) {
|
||||
window.open(url);
|
||||
@@ -430,23 +435,23 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
|
||||
onLegendSelectChanged(e) {
|
||||
this.selected = e.selected;
|
||||
this.right = this.selected['Fiat'] ? +this.initialRight + 40 : this.initialRight;
|
||||
this.left = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? this.initialLeft : +this.initialLeft - 40;
|
||||
this.adjustedRight = this.selected['Fiat'] ? +this.right + 40 : +this.right;
|
||||
this.adjustedLeft = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? +this.left : +this.left - 40;
|
||||
|
||||
this.chartOptions = {
|
||||
grid: {
|
||||
right: this.right,
|
||||
left: this.left,
|
||||
right: this.adjustedRight,
|
||||
left: this.adjustedLeft,
|
||||
},
|
||||
legend: {
|
||||
selected: this.selected,
|
||||
},
|
||||
dataZoom: this.allowZoom ? [{
|
||||
left: this.left,
|
||||
right: this.right,
|
||||
left: this.adjustedLeft,
|
||||
right: this.adjustedRight,
|
||||
}, {
|
||||
left: this.left,
|
||||
right: this.right,
|
||||
left: this.adjustedLeft,
|
||||
right: this.adjustedRight,
|
||||
}] : undefined
|
||||
};
|
||||
|
||||
@@ -478,7 +483,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
// Add a point at today's date to make the graph end at the current time
|
||||
extendedSummary.unshift({ time: Date.now() / 1000, value: 0 });
|
||||
extendedSummary.reverse();
|
||||
|
||||
|
||||
let oneHour = 60 * 60;
|
||||
// Fill gaps longer than interval
|
||||
for (let i = 0; i < extendedSummary.length - 1; i++) {
|
||||
@@ -491,7 +496,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
i += hours - 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return extendedSummary.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="timestamp" *ngIf="!widget" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="block.timestamp" [hideTimeSince]="true"></app-timestamp>
|
||||
</td>
|
||||
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<a
|
||||
|
||||
@@ -56,8 +56,7 @@
|
||||
</ng-template>
|
||||
</td>
|
||||
<td class="timestamp text-left">
|
||||
‎{{ utxo.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||
<div class="symbol lg-inline relative-time"><i>(<app-time kind="since" [time]="utxo.blocktime"></app-time>)</i></div>
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm'" [unixTime]="utxo.blocktime"></app-timestamp>
|
||||
</td>
|
||||
<td class="expires-in text-left" [ngStyle]="{ 'color': getGradientColor(utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate) }">
|
||||
{{ utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate < 0 ? -(utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate) : utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate }} <span i18n="shared.blocks" class="symbol">blocks</span>
|
||||
|
||||
@@ -53,8 +53,7 @@
|
||||
</ng-container>
|
||||
</td>
|
||||
<td class="timestamp text-left">
|
||||
‎{{ peg.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||
<div class="symbol lg-inline relative-time"><i>(<app-time kind="since" [time]="peg.blocktime"></app-time>)</i></div>
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm'" [unixTime]="peg.blocktime"></app-timestamp>
|
||||
</td>
|
||||
<td class="amount text-right" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0, 'glow-effect': peg.amount < 0 && peg.bitcoinaddress && !peg.bitcointxid}">
|
||||
<app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true" [addPlus]="true"></app-amount>
|
||||
|
||||
@@ -194,7 +194,7 @@
|
||||
<a [routerLink]="['/block' | relativeUrl, block.id]">{{ block.height }}</a>
|
||||
</td>
|
||||
<td class="timestamp">
|
||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="block.timestamp" [hideTimeSince]="true"></app-timestamp>
|
||||
</td>
|
||||
<td class="mined">
|
||||
<app-time kind="since" [time]="block.timestamp" [fastRender]="true" [showTooltip]="true"></app-time>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
.accept-results {
|
||||
td, th {
|
||||
&.allowed {
|
||||
width: 10%;
|
||||
text-align: center;
|
||||
}
|
||||
&.txid {
|
||||
width: 50%;
|
||||
}
|
||||
&.rate {
|
||||
width: 20%;
|
||||
text-align: right;
|
||||
white-space: wrap;
|
||||
}
|
||||
&.reason {
|
||||
width: 20%;
|
||||
text-align: right;
|
||||
white-space: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 950px) {
|
||||
table-layout: auto;
|
||||
|
||||
td, th {
|
||||
&.allowed {
|
||||
width: 100px;
|
||||
}
|
||||
&.txid {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,9 @@
|
||||
<th class="rtt only-small">RTT</th>
|
||||
<th class="rtt only-large">RTT</th>
|
||||
<th class="height">Height</th>
|
||||
<th class="frontend only-large">Front</th>
|
||||
<th class="backend only-large">Back</th>
|
||||
<th class="electrs only-large">Electrs</th>
|
||||
</tr>
|
||||
<tr *ngFor="let host of hosts; let i = index; trackBy: trackByFn">
|
||||
<td class="rank">{{ i + 1 }}</td>
|
||||
@@ -28,6 +31,15 @@
|
||||
<td class="rtt only-small">{{ (host.rtt / 1000) | number : '1.1-1' }} {{ host.rtt == null ? '' : 's'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
|
||||
<td class="rtt only-large">{{ host.rtt | number : '1.0-0' }} {{ host.rtt == null ? '' : 'ms'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
|
||||
<td class="height">{{ host.latestHeight }} {{ !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '✅')) }}</td>
|
||||
<ng-container *ngFor="let type of ['frontend', 'backend', 'electrs']">
|
||||
<td class="{{type}} only-large" [style.background-color]="host.hashes?.[type] ? '#' + host.hashes[type].slice(0, 6) : ''">
|
||||
@if (host.hashes?.[type]) {
|
||||
<a [style.color]="'white'" href="https://github.com/mempool/{{type === 'electrs' ? 'electrs' : 'mempool'}}/commit/{{ host.hashes[type] }}" target="_blank">{{ host.hashes[type].slice(0, 8) || '?' }}</a>
|
||||
} @else {
|
||||
<span>?</span>
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
}
|
||||
|
||||
.status-panel {
|
||||
max-width: 720px;
|
||||
max-width: 1000px;
|
||||
margin: auto;
|
||||
padding: 1em;
|
||||
background: var(--box-bg);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<div [formGroup]="timezoneForm" class="text-small text-center">
|
||||
<select formControlName="mode" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 110px;" (change)="changeMode()">
|
||||
<option value="local">UTC{{ localTimezoneOffset !== '+0' ? localTimezoneOffset : '' }} {{ localTimezoneName ? '- ' + localTimezoneName : '' }}</option>
|
||||
<option value="+0" *ngIf="localTimezoneOffset !== '+0'">UTC - Greenwich Mean Time (GMT)</option>
|
||||
<option disabled>────</option>
|
||||
<option *ngFor="let timezone of timezones" [value]="timezone.offset">UTC{{ timezone.offset !== '+0' ? timezone.offset : '' }} - {{ timezone.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -0,0 +1,58 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { StorageService } from '@app/services/storage.service';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { timezones } from '@app/app.constants';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-timezone-selector',
|
||||
templateUrl: './timezone-selector.component.html',
|
||||
styleUrls: ['./timezone-selector.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class TimezoneSelectorComponent implements OnInit {
|
||||
timezoneForm: UntypedFormGroup;
|
||||
timezones = timezones;
|
||||
localTimezoneOffset: string = '';
|
||||
localTimezoneName: string;
|
||||
|
||||
constructor(
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private stateService: StateService,
|
||||
private storageService: StorageService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.setLocalTimezone();
|
||||
this.timezoneForm = this.formBuilder.group({
|
||||
mode: ['local'],
|
||||
});
|
||||
this.stateService.timezone$.subscribe((mode) => {
|
||||
this.timezoneForm.get('mode')?.setValue(mode);
|
||||
});
|
||||
}
|
||||
|
||||
changeMode() {
|
||||
const newMode = this.timezoneForm.get('mode')?.value;
|
||||
this.storageService.setValue('timezone-preference', newMode);
|
||||
this.stateService.timezone$.next(newMode);
|
||||
}
|
||||
|
||||
setLocalTimezone() {
|
||||
const offset = new Date().getTimezoneOffset();
|
||||
const sign = offset <= 0 ? "+" : "-";
|
||||
const absOffset = Math.abs(offset);
|
||||
const hours = String(Math.floor(absOffset / 60));
|
||||
const minutes = String(absOffset % 60).padStart(2, '0');
|
||||
if (minutes === '00') {
|
||||
this.localTimezoneOffset = `${sign}${hours}`;
|
||||
} else {
|
||||
this.localTimezoneOffset = `${sign}${hours.padStart(2, '0')}:${minutes}`;
|
||||
}
|
||||
|
||||
const timezone = this.timezones.find(tz => tz.offset === this.localTimezoneOffset);
|
||||
this.timezones = this.timezones.filter(tz => tz.offset !== this.localTimezoneOffset && tz.offset !== '+0');
|
||||
this.localTimezoneName = timezone ? timezone.name : '';
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@
|
||||
<div class="field narrower mt-2">
|
||||
<div class="label" i18n="transaction.confirmed-at">Confirmed at</div>
|
||||
<div class="value">
|
||||
‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm'" [unixTime]="tx.status.block_time" [hideTimeSince]="true"></app-timestamp>
|
||||
<div class="lg-inline">
|
||||
<i class="symbol">(<app-time kind="since" [time]="tx.status.block_time" [fastRender]="true" [showTooltip]="true"></app-time>)</i>
|
||||
</div>
|
||||
|
||||
@@ -61,10 +61,7 @@
|
||||
<tr>
|
||||
<td i18n="block.timestamp">Timestamp</td>
|
||||
<td>
|
||||
‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
|
||||
<div class="lg-inline">
|
||||
<i class="symbol">(<app-time kind="since" [time]="tx.status.block_time" [fastRender]="true"></app-time>)</i>
|
||||
</div>
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="tx.status.block_time"></app-timestamp>
|
||||
</td>
|
||||
</tr>
|
||||
} @else {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<app-truncate [text]="tx.txid"></app-truncate>
|
||||
</a>
|
||||
<div>
|
||||
<ng-template [ngIf]="tx.status.confirmed">‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}</ng-template>
|
||||
<ng-template [ngIf]="tx.status.confirmed"><app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="tx.status.block_time" [hideTimeSince]="true"></app-timestamp></ng-template>
|
||||
<ng-template [ngIf]="!tx.status.confirmed && tx.firstSeen">
|
||||
<i><app-time kind="since" [time]="tx.firstSeen" [fastRender]="true" [showTooltip]="true"></app-time></i>
|
||||
</ng-template>
|
||||
@@ -81,7 +81,7 @@
|
||||
</ng-container>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right nowrap amount" [class]="{large: vin?.prevout?.value > 1000000000 || vin.isInscription}">
|
||||
<td class="text-right nowrap amount" [class]="{large: tx.largeInput}">
|
||||
<button *ngIf="vin.isInscription" (click)="toggleOrdData(tx.txid, 'vin', vindex)" type="button" class="btn btn-sm badge badge-ord primary" style="margin-right: 10px;">Inscription</button>
|
||||
<ng-template [ngIf]="vin.prevout && vin.prevout.asset && vin.prevout.asset !== nativeAssetId" [ngIfElse]="defaultOutput">
|
||||
<div *ngIf="assetsMinimal && assetsMinimal[vin.prevout.asset] else assetVinNotFound">
|
||||
@@ -257,7 +257,7 @@
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td class="text-right nowrap amount" [class]="{large: vout?.value > 1000000000}">
|
||||
<td class="text-right nowrap amount" [class]="{large: tx.largeOutput}">
|
||||
<ng-template [ngIf]="vout.asset && vout.asset !== nativeAssetId" [ngIfElse]="defaultOutput">
|
||||
<div *ngIf="assetsMinimal && assetsMinimal[vout.asset] else assetNotFound">
|
||||
<ng-container *ngTemplateOutlet="assetBox; context:{ $implicit: vout }"></ng-container>
|
||||
|
||||
@@ -262,6 +262,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
const hasAnnex = tx.vin[i].witness?.[tx.vin[i].witness.length - 1].startsWith('50');
|
||||
if (tx.vin[i].witness.length > (hasAnnex ? 2 : 1) && tx.vin[i].witness[tx.vin[i].witness.length - (hasAnnex ? 3 : 2)].includes('0063036f7264')) {
|
||||
tx.vin[i].isInscription = true;
|
||||
tx.largeInput = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,6 +273,9 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tx.largeInput = tx.largeInput || tx.vin.some(vin => (vin?.prevout?.value > 1000000000));
|
||||
tx.largeOutput = tx.vout.some(vout => (vout?.value > 1000000000));
|
||||
});
|
||||
|
||||
if (this.blockTime && this.transactions?.length && this.currency) {
|
||||
@@ -355,8 +359,12 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
this.electrsApiService.getTransaction$(tx.txid)
|
||||
.subscribe((newTx) => {
|
||||
tx['@vinLoaded'] = true;
|
||||
let temp = tx.vin;
|
||||
tx.vin = newTx.vin;
|
||||
tx.fee = newTx.fee;
|
||||
for (const [index, vin] of temp.entries()) {
|
||||
newTx.vin[index].isInscription = vin.isInscription;
|
||||
}
|
||||
this.ref.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<div class="box preview-box" *ngIf="(walletAddresses$ | async) as walletAddresses">
|
||||
<app-preview-title>
|
||||
<span i18n="shared.wallet">Wallet</span>
|
||||
</app-preview-title>
|
||||
<div>
|
||||
<div class="table-col">
|
||||
<table class="table table-borderless dual-col-striped table-fixed wallet-table" *ngIf="(walletStats$ | async) as walletStats">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="address.number-addresses">Addresses</td>
|
||||
<td class="wrap-cell">{{ addressStrings.length }}</td>
|
||||
<td class="spacer"></td>
|
||||
<td i18n="address.utxos">UTXOs</td>
|
||||
<td class="wrap-cell">{{ walletStats.utxos }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="wallet.balance-btc">Balance (BTC)</td>
|
||||
<td class="wrap-cell"><app-amount [satoshis]="walletStats.balance" [noFiat]="true" [digitsInfo]="walletStats.balance > 1_000_000_000 ? '1.4-4' : '1.8-8'"></app-amount></td>
|
||||
<td class="spacer"></td>
|
||||
<td i18n="wallet.balance-usd">Balance (USD)</td>
|
||||
<td class="wrap-cell"><span class="fiat"><app-fiat [value]="walletStats.balance"></app-fiat></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="w-100 d-block d-md-none"></div>
|
||||
<div class="col-md graph-col">
|
||||
<app-address-graph [addressSummary$]="walletSummary$" period="all" [widget]="true" [defaultFiat]="true" [height]="330" [left]="-40" [right]="-40" [showLegend]="false" [showYAxis]="false"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,31 @@
|
||||
.title-wrapper {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.graph-col {
|
||||
height: 350px;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
margin-left: 2px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.table-col {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table {
|
||||
font-size: 32px;
|
||||
|
||||
::ng-deep .symbol {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fiat {
|
||||
display: block;
|
||||
}
|
||||
245
frontend/src/app/components/wallet/wallet-preview.component.ts
Normal file
245
frontend/src/app/components/wallet/wallet-preview.component.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { switchMap, catchError, map, tap, shareReplay, startWith, scan } from 'rxjs/operators';
|
||||
import { Address, AddressTxSummary, ChainStats, Transaction } from '@interfaces/electrs.interface';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { ApiService } from '@app/services/api.service';
|
||||
import { of, Observable, Subscription } from 'rxjs';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { seoDescriptionNetwork } from '@app/shared/common.utils';
|
||||
import { WalletAddress } from '@interfaces/node-api.interface';
|
||||
import { OpenGraphService } from '../../services/opengraph.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
|
||||
class WalletStats implements ChainStats {
|
||||
addresses: string[];
|
||||
funded_txo_count: number;
|
||||
funded_txo_sum: number;
|
||||
spent_txo_count: number;
|
||||
spent_txo_sum: number;
|
||||
tx_count: number;
|
||||
|
||||
constructor (stats: ChainStats[], addresses: string[]) {
|
||||
Object.assign(this, stats.reduce((acc, stat) => {
|
||||
acc.funded_txo_count += stat.funded_txo_count;
|
||||
acc.funded_txo_sum += stat.funded_txo_sum;
|
||||
acc.spent_txo_count += stat.spent_txo_count;
|
||||
acc.spent_txo_sum += stat.spent_txo_sum;
|
||||
return acc;
|
||||
}, {
|
||||
funded_txo_count: 0,
|
||||
funded_txo_sum: 0,
|
||||
spent_txo_count: 0,
|
||||
spent_txo_sum: 0,
|
||||
tx_count: 0,
|
||||
})
|
||||
);
|
||||
this.addresses = addresses;
|
||||
}
|
||||
|
||||
public addTx(tx: Transaction): void {
|
||||
for (const vin of tx.vin) {
|
||||
if (this.addresses.includes(vin.prevout?.scriptpubkey_address)) {
|
||||
this.spendTxo(vin.prevout.value);
|
||||
}
|
||||
}
|
||||
for (const vout of tx.vout) {
|
||||
if (this.addresses.includes(vout.scriptpubkey_address)) {
|
||||
this.fundTxo(vout.value);
|
||||
}
|
||||
}
|
||||
this.tx_count++;
|
||||
}
|
||||
|
||||
public removeTx(tx: Transaction): void {
|
||||
for (const vin of tx.vin) {
|
||||
if (this.addresses.includes(vin.prevout?.scriptpubkey_address)) {
|
||||
this.unspendTxo(vin.prevout.value);
|
||||
}
|
||||
}
|
||||
for (const vout of tx.vout) {
|
||||
if (this.addresses.includes(vout.scriptpubkey_address)) {
|
||||
this.unfundTxo(vout.value);
|
||||
}
|
||||
}
|
||||
this.tx_count--;
|
||||
}
|
||||
|
||||
private fundTxo(value: number): void {
|
||||
this.funded_txo_sum += value;
|
||||
this.funded_txo_count++;
|
||||
}
|
||||
|
||||
private unfundTxo(value: number): void {
|
||||
this.funded_txo_sum -= value;
|
||||
this.funded_txo_count--;
|
||||
}
|
||||
|
||||
private spendTxo(value: number): void {
|
||||
this.spent_txo_sum += value;
|
||||
this.spent_txo_count++;
|
||||
}
|
||||
|
||||
private unspendTxo(value: number): void {
|
||||
this.spent_txo_sum -= value;
|
||||
this.spent_txo_count--;
|
||||
}
|
||||
|
||||
get balance(): number {
|
||||
return this.funded_txo_sum - this.spent_txo_sum;
|
||||
}
|
||||
|
||||
get totalReceived(): number {
|
||||
return this.funded_txo_sum;
|
||||
}
|
||||
|
||||
get utxos(): number {
|
||||
return this.funded_txo_count - this.spent_txo_count;
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-wallet-preview',
|
||||
templateUrl: './wallet-preview.component.html',
|
||||
styleUrls: ['./wallet-preview.component.scss']
|
||||
})
|
||||
export class WalletPreviewComponent implements OnInit, OnDestroy {
|
||||
network = '';
|
||||
|
||||
addresses: Address[] = [];
|
||||
addressStrings: string[] = [];
|
||||
walletName: string;
|
||||
isLoadingWallet = true;
|
||||
wallet$: Observable<Record<string, WalletAddress>>;
|
||||
walletAddresses$: Observable<Record<string, Address>>;
|
||||
walletSummary$: Observable<AddressTxSummary[]>;
|
||||
walletStats$: Observable<WalletStats>;
|
||||
error: any;
|
||||
walletSubscription: Subscription;
|
||||
|
||||
collapseAddresses: boolean = true;
|
||||
|
||||
fullyLoaded = false;
|
||||
txCount = 0;
|
||||
received = 0;
|
||||
sent = 0;
|
||||
chainBalance = 0;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private stateService: StateService,
|
||||
private apiService: ApiService,
|
||||
private seoService: SeoService,
|
||||
private websocketService: WebsocketService,
|
||||
private openGraphService: OpenGraphService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks', 'stats']);
|
||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||
this.wallet$ = this.route.paramMap.pipe(
|
||||
map((params: ParamMap) => params.get('wallet') as string),
|
||||
tap((walletName: string) => {
|
||||
this.walletName = walletName;
|
||||
this.openGraphService.waitFor('wallet-addresses-' + this.walletName);
|
||||
this.openGraphService.waitFor('wallet-data-' + this.walletName);
|
||||
this.openGraphService.waitFor('wallet-txs-' + this.walletName);
|
||||
this.seoService.setTitle($localize`:@@wallet.component.browser-title:Wallet: ${walletName}:INTERPOLATION:`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.wallet:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} wallet ${walletName}:INTERPOLATION:.`);
|
||||
}),
|
||||
switchMap((walletName: string) => this.apiService.getWallet$(walletName).pipe(
|
||||
catchError((err) => {
|
||||
this.error = err;
|
||||
this.seoService.logSoft404();
|
||||
console.log(err);
|
||||
this.openGraphService.fail('wallet-addresses-' + this.walletName);
|
||||
this.openGraphService.fail('wallet-data-' + this.walletName);
|
||||
this.openGraphService.fail('wallet-txs-' + this.walletName);
|
||||
return of({});
|
||||
})
|
||||
)),
|
||||
shareReplay(1),
|
||||
);
|
||||
|
||||
this.walletAddresses$ = this.wallet$.pipe(
|
||||
map(wallet => {
|
||||
const walletInfo: Record<string, Address> = {};
|
||||
for (const address of Object.keys(wallet)) {
|
||||
walletInfo[address] = {
|
||||
address,
|
||||
chain_stats: wallet[address].stats,
|
||||
mempool_stats: {
|
||||
funded_txo_count: 0,
|
||||
funded_txo_sum: 0,
|
||||
spent_txo_count: 0, spent_txo_sum: 0, tx_count: 0
|
||||
},
|
||||
};
|
||||
}
|
||||
return walletInfo;
|
||||
}),
|
||||
tap(() => {
|
||||
this.isLoadingWallet = false;
|
||||
})
|
||||
);
|
||||
|
||||
this.walletSubscription = this.walletAddresses$.subscribe(wallet => {
|
||||
this.addressStrings = Object.keys(wallet);
|
||||
this.addresses = Object.values(wallet);
|
||||
this.openGraphService.waitOver('wallet-addresses-' + this.walletName);
|
||||
});
|
||||
|
||||
this.walletSummary$ = this.wallet$.pipe(
|
||||
map(wallet => this.deduplicateWalletTransactions(Object.values(wallet).flatMap(address => address.transactions))),
|
||||
tap(() => {
|
||||
this.openGraphService.waitOver('wallet-txs-' + this.walletName);
|
||||
})
|
||||
);
|
||||
|
||||
this.walletStats$ = this.wallet$.pipe(
|
||||
switchMap(wallet => {
|
||||
const walletStats = new WalletStats(Object.values(wallet).map(w => w.stats), Object.keys(wallet));
|
||||
return this.stateService.walletTransactions$.pipe(
|
||||
startWith([]),
|
||||
scan((stats, newTransactions) => {
|
||||
for (const tx of newTransactions) {
|
||||
stats.addTx(tx);
|
||||
}
|
||||
return stats;
|
||||
}, walletStats),
|
||||
);
|
||||
}),
|
||||
tap(() => {
|
||||
this.openGraphService.waitOver('wallet-data-' + this.walletName);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
deduplicateWalletTransactions(walletTransactions: AddressTxSummary[]): AddressTxSummary[] {
|
||||
const transactions = new Map<string, AddressTxSummary>();
|
||||
for (const tx of walletTransactions) {
|
||||
if (transactions.has(tx.txid)) {
|
||||
transactions.get(tx.txid).value += tx.value;
|
||||
} else {
|
||||
transactions.set(tx.txid, tx);
|
||||
}
|
||||
}
|
||||
return Array.from(transactions.values()).sort((a, b) => {
|
||||
if (a.height === b.height) {
|
||||
return b.tx_position - a.tx_position;
|
||||
}
|
||||
return b.height - a.height;
|
||||
});
|
||||
}
|
||||
|
||||
normalizeAddress(address: string): string {
|
||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(address)) {
|
||||
return address.toLowerCase();
|
||||
} else {
|
||||
return address;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.walletSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import { HashrateChartPoolsComponent } from '@components/hashrates-chart-pools/h
|
||||
import { BlockHealthGraphComponent } from '@components/block-health-graph/block-health-graph.component';
|
||||
import { AddressComponent } from '@components/address/address.component';
|
||||
import { WalletComponent } from '@components/wallet/wallet.component';
|
||||
import { WalletPreviewComponent } from '@components/wallet/wallet-preview.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';
|
||||
@@ -49,6 +50,7 @@ import { CommonModule } from '@angular/common';
|
||||
MempoolBlockComponent,
|
||||
AddressComponent,
|
||||
WalletComponent,
|
||||
WalletPreviewComponent,
|
||||
|
||||
MiningDashboardComponent,
|
||||
AcceleratorDashboardComponent,
|
||||
|
||||
@@ -32,6 +32,8 @@ export interface Transaction {
|
||||
price?: Price;
|
||||
sigops?: number;
|
||||
flags?: bigint;
|
||||
largeInput?: boolean;
|
||||
largeOutput?: boolean;
|
||||
}
|
||||
|
||||
export interface TransactionChannels {
|
||||
|
||||
@@ -144,4 +144,9 @@ export interface HealthCheckHost {
|
||||
link?: string;
|
||||
statusPage?: SafeResourceUrl;
|
||||
flag?: string;
|
||||
hashes?: {
|
||||
frontend?: string;
|
||||
backend?: string;
|
||||
electrs?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="lightning.created">Created</td>
|
||||
<td>{{ channel.created | date:'yyyy-MM-dd HH:mm' }}</td>
|
||||
<td><app-timestamp [customFormat]="'yyyy-MM-dd HH:mm'" [unixTime]="channel.created" [hideTimeSince]="true"></app-timestamp></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.capacity">Capacity</td>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<ng-container *ngFor="let channel of channels;">
|
||||
<tr>
|
||||
<td class="timestamp">
|
||||
‎{{ channel.closing_date | date:'yyyy-MM-dd HH:mm' }}
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm'" [unixTime]="channel.closing_date" [hideTimeSince]="true"></app-timestamp>
|
||||
</td>
|
||||
<td class="capacity text-right">
|
||||
<app-amount *ngIf="channel.capacity > 100000000; else smallnode" [satoshis]="channel.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||
|
||||
@@ -142,12 +142,12 @@ const routes: Routes = [
|
||||
|
||||
if (window['__env']?.OFFICIAL_MEMPOOL_SPACE) {
|
||||
routes[0].children.push({
|
||||
path: 'nodes',
|
||||
path: 'monitoring',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: ServerHealthComponent
|
||||
});
|
||||
routes[0].children.push({
|
||||
path: 'network',
|
||||
path: 'nodes',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: ServerStatusComponent
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { RouterModule, Routes } from '@angular/router';
|
||||
import { TransactionPreviewComponent } from '@components/transaction/transaction-preview.component';
|
||||
import { BlockPreviewComponent } from '@components/block/block-preview.component';
|
||||
import { AddressPreviewComponent } from '@components/address/address-preview.component';
|
||||
import { WalletPreviewComponent } from '@components/wallet/wallet-preview.component';
|
||||
import { PoolPreviewComponent } from '@components/pool/pool-preview.component';
|
||||
import { MasterPagePreviewComponent } from '@components/master-page-preview/master-page-preview.component';
|
||||
|
||||
@@ -20,6 +21,11 @@ const routes: Routes = [
|
||||
children: [],
|
||||
component: AddressPreviewComponent
|
||||
},
|
||||
{
|
||||
path: 'wallet/:wallet',
|
||||
children: [],
|
||||
component: WalletPreviewComponent
|
||||
},
|
||||
{
|
||||
path: 'tx/:id',
|
||||
children: [],
|
||||
|
||||
@@ -55,7 +55,7 @@ export class EtaService {
|
||||
|
||||
return {
|
||||
hashratePercentage: acceleratingHashrateFraction * 100,
|
||||
ETA: Date.now() + da.timeAvg * mempoolPosition.block,
|
||||
ETA: Date.now() + da.adjustedTimeAvg * mempoolPosition.block,
|
||||
acceleratedETA: this.calculateETAFromShares([
|
||||
{ block: mempoolPosition.block, hashrateShare: (1 - acceleratingHashrateFraction) },
|
||||
{ block: 0, hashrateShare: acceleratingHashrateFraction },
|
||||
@@ -216,7 +216,7 @@ export class EtaService {
|
||||
}
|
||||
// at max depth, the transaction is guaranteed to be mined in the next block if it hasn't already
|
||||
Q += ((max + 1) * (1-tailProb));
|
||||
const eta = da.timeAvg * Q; // T x Q
|
||||
const eta = da.adjustedTimeAvg * Q; // T x Q
|
||||
|
||||
return {
|
||||
now,
|
||||
|
||||
@@ -143,8 +143,8 @@ export class ServicesApiServices {
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
|
||||
}
|
||||
|
||||
accelerateWithGooglePay$(txInput: string, token: string, cardTag: string, referenceId: string, userApprovedUSD: number) {
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
|
||||
accelerateWithGooglePay$(txInput: string, token: string, verificationToken: string, cardTag: string, referenceId: string, userApprovedUSD: number) {
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
|
||||
}
|
||||
|
||||
getAccelerations$(): Observable<Acceleration[]> {
|
||||
|
||||
@@ -186,6 +186,7 @@ export class StateService {
|
||||
live2Chart$ = new Subject<OptimizedMempoolStats>();
|
||||
|
||||
viewAmountMode$: BehaviorSubject<'btc' | 'sats' | 'fiat'>;
|
||||
timezone$: BehaviorSubject<string>;
|
||||
connectionState$ = new BehaviorSubject<0 | 1 | 2>(2);
|
||||
isTabHidden$: Observable<boolean>;
|
||||
|
||||
@@ -347,6 +348,9 @@ export class StateService {
|
||||
const viewAmountModePreference = this.storageService.getValue('view-amount-mode') as 'btc' | 'sats' | 'fiat';
|
||||
this.viewAmountMode$ = new BehaviorSubject<'btc' | 'sats' | 'fiat'>(viewAmountModePreference || 'btc');
|
||||
|
||||
const timezonePreference = this.storageService.getValue('timezone-preference');
|
||||
this.timezone$ = new BehaviorSubject<string>(timezonePreference || 'local');
|
||||
|
||||
this.backend$.subscribe(backend => {
|
||||
this.backend = backend;
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<app-fiat-selector></app-fiat-selector>
|
||||
</div>
|
||||
<div class="selector">
|
||||
<app-rate-unit-selector></app-rate-unit-selector>
|
||||
<app-timezone-selector></app-timezone-selector>
|
||||
</div>
|
||||
<div class="selector d-none" [ngClass]="isServicesPage ? 'd-lg-flex' : 'd-md-flex'">
|
||||
<app-amount-selector></app-amount-selector>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<span *ngIf="seconds === undefined">-</span>
|
||||
<span *ngIf="seconds !== undefined">
|
||||
‎{{ seconds * 1000 | date: customFormat ?? 'yyyy-MM-dd HH:mm' }}
|
||||
‎{{ seconds * 1000 | date: customFormat ?? 'yyyy-MM-dd HH:mm' : (stateService.timezone$ | async) }}
|
||||
<div class="lg-inline" *ngIf="!hideTimeSince">
|
||||
<i class="symbol">(<app-time kind="since" [time]="seconds" [fastRender]="true" [precision]="precision" [minUnit]="minUnit"></app-time>)</i>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-timestamp',
|
||||
@@ -16,6 +17,10 @@ export class TimestampComponent implements OnChanges {
|
||||
|
||||
seconds: number | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnChanges(): void {
|
||||
if (this.unixTime) {
|
||||
this.seconds = this.unixTime;
|
||||
|
||||
@@ -8,8 +8,12 @@ export class AmountShortenerPipe implements PipeTransform {
|
||||
const digits = args[0] ?? 1;
|
||||
const unit = args[1] || undefined;
|
||||
const isMoney = args[2] || false;
|
||||
const sigfigs = args[3] || false; // if true, "digits" is the number of significant digits, not the number of decimal places
|
||||
|
||||
if (num < 1000) {
|
||||
if (sigfigs) {
|
||||
return Number(num.toPrecision(digits));
|
||||
}
|
||||
return num.toFixed(digits);
|
||||
}
|
||||
|
||||
@@ -25,10 +29,15 @@ export class AmountShortenerPipe implements PipeTransform {
|
||||
const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
|
||||
const item = lookup.slice().reverse().find((item) => num >= item.value);
|
||||
|
||||
if (unit !== undefined) {
|
||||
return item ? (num / item.value).toFixed(digits).replace(rx, '$1') + ' ' + item.symbol + unit : '0';
|
||||
} else {
|
||||
return item ? (num / item.value).toFixed(digits).replace(rx, '$1') + item.symbol : '0';
|
||||
if (!item) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
const scaledNum = num / item.value;
|
||||
const formattedNum = Number(sigfigs ? scaledNum.toPrecision(digits) : scaledNum.toFixed(digits)).toString();
|
||||
|
||||
return unit !== undefined
|
||||
? formattedNum + ' ' + item.symbol + unit
|
||||
: formattedNum + item.symbol;
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import { FiatSelectorComponent } from '@components/fiat-selector/fiat-selector.c
|
||||
import { RateUnitSelectorComponent } from '@components/rate-unit-selector/rate-unit-selector.component';
|
||||
import { ThemeSelectorComponent } from '@components/theme-selector/theme-selector.component';
|
||||
import { AmountSelectorComponent } from '@components/amount-selector/amount-selector.component';
|
||||
import { TimezoneSelectorComponent } from '@components/timezone-selector/timezone-selector.component';
|
||||
import { BrowserOnlyDirective } from '@app/shared/directives/browser-only.directive';
|
||||
import { ServerOnlyDirective } from '@app/shared/directives/server-only.directive';
|
||||
import { ColoredPriceDirective } from '@app/shared/directives/colored-price.directive';
|
||||
@@ -134,6 +135,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
|
||||
ThemeSelectorComponent,
|
||||
RateUnitSelectorComponent,
|
||||
AmountSelectorComponent,
|
||||
TimezoneSelectorComponent,
|
||||
ScriptpubkeyTypePipe,
|
||||
RelativeUrlPipe,
|
||||
NoSanitizePipe,
|
||||
@@ -283,6 +285,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
|
||||
RateUnitSelectorComponent,
|
||||
ThemeSelectorComponent,
|
||||
AmountSelectorComponent,
|
||||
TimezoneSelectorComponent,
|
||||
ScriptpubkeyTypePipe,
|
||||
RelativeUrlPipe,
|
||||
Hex2asciiPipe,
|
||||
|
||||
Reference in New Issue
Block a user