Merge branch 'master' into nymkappa/network-switch-align

This commit is contained in:
wiz
2023-07-18 15:40:57 +09:00
committed by GitHub
56 changed files with 7427 additions and 8466 deletions

View File

@@ -22,6 +22,5 @@
"TESTNET_BLOCK_AUDIT_START_HEIGHT": 0,
"SIGNET_BLOCK_AUDIT_START_HEIGHT": 0,
"LIGHTNING": false,
"FULL_RBF_ENABLED": false,
"HISTORICAL_PRICE": true
}

15031
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -61,60 +61,60 @@
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
},
"dependencies": {
"@angular-devkit/build-angular": "^14.2.10",
"@angular/animations": "^14.2.12",
"@angular/cli": "^14.2.10",
"@angular/common": "^14.2.12",
"@angular/compiler": "^14.2.12",
"@angular/core": "^14.2.12",
"@angular/forms": "^14.2.12",
"@angular/localize": "^14.2.12",
"@angular/platform-browser": "^14.2.12",
"@angular/platform-browser-dynamic": "^14.2.12",
"@angular/platform-server": "^14.2.12",
"@angular/router": "^14.2.12",
"@fortawesome/angular-fontawesome": "~0.11.1",
"@fortawesome/fontawesome-common-types": "~6.2.1",
"@fortawesome/fontawesome-svg-core": "~6.2.1",
"@fortawesome/free-solid-svg-icons": "~6.2.1",
"@angular-devkit/build-angular": "^16.1.4",
"@angular/animations": "^16.1.5",
"@angular/cli": "^16.1.4",
"@angular/common": "^16.1.5",
"@angular/compiler": "^16.1.5",
"@angular/core": "^16.1.5",
"@angular/forms": "^16.1.5",
"@angular/localize": "^16.1.5",
"@angular/platform-browser": "^16.1.5",
"@angular/platform-browser-dynamic": "^16.1.5",
"@angular/platform-server": "^16.1.5",
"@angular/router": "^16.1.5",
"@fortawesome/angular-fontawesome": "~0.13.0",
"@fortawesome/fontawesome-common-types": "~6.4.0",
"@fortawesome/fontawesome-svg-core": "~6.4.0",
"@fortawesome/free-solid-svg-icons": "~6.4.0",
"@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^13.1.1",
"@ng-bootstrap/ng-bootstrap": "^15.1.0",
"@types/qrcode": "~1.5.0",
"bootstrap": "~4.6.1",
"bootstrap": "~4.6.2",
"browserify": "^17.0.0",
"clipboard": "^2.0.11",
"domino": "^2.1.6",
"echarts": "~5.4.1",
"echarts": "~5.4.3",
"echarts-gl": "^2.0.9",
"lightweight-charts": "~3.8.0",
"ngx-echarts": "~14.0.0",
"ngx-infinite-scroll": "^14.0.1",
"ngx-echarts": "~16.0.0",
"ngx-infinite-scroll": "^16.0.0",
"qrcode": "1.5.1",
"rxjs": "~7.8.0",
"tinyify": "^3.1.0",
"rxjs": "~7.8.1",
"tinyify": "^4.0.0",
"tlite": "^0.1.9",
"tslib": "~2.4.1",
"zone.js": "~0.12.0"
"tslib": "~2.6.0",
"zone.js": "~0.13.1"
},
"devDependencies": {
"@angular/compiler-cli": "^14.2.12",
"@angular/language-service": "^14.2.12",
"@angular/compiler-cli": "^16.1.5",
"@angular/language-service": "^16.1.5",
"@types/node": "^18.11.9",
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"eslint": "^8.31.0",
"http-proxy-middleware": "~2.0.6",
"prettier": "^2.8.2",
"prettier": "^3.0.0",
"ts-node": "~10.9.1",
"typescript": "~4.6.4"
"typescript": "~4.9.3"
},
"optionalDependencies": {
"@cypress/schematic": "^2.4.0",
"cypress": "^12.7.0",
"cypress-fail-on-console-error": "~4.0.2",
"@cypress/schematic": "^2.5.0",
"cypress": "^12.17.1",
"cypress-fail-on-console-error": "~4.0.3",
"cypress-wait-until": "^1.7.2",
"mock-socket": "~9.1.5",
"start-server-and-test": "~1.14.0"
"mock-socket": "~9.2.1",
"start-server-and-test": "~2.0.0"
},
"scarfSettings": {
"enabled": false

View File

@@ -1,4 +1,4 @@
import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser';
import { BrowserModule } from '@angular/platform-browser';
import { ModuleWithProviders, NgModule } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@@ -48,8 +48,7 @@ const providers = [
AppComponent,
],
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),
BrowserTransferStateModule,
BrowserModule,
AppRoutingModule,
HttpClientModule,
BrowserAnimationsModule,

View File

@@ -207,7 +207,7 @@ export class AddressComponent implements OnInit, OnDestroy {
}
this.isLoadingTransactions = true;
this.retryLoadMore = false;
this.electrsApiService.getAddressTransactionsFromHash$(this.address.address, this.lastTransactionTxId)
this.electrsApiService.getAddressTransactions$(this.address.address, this.lastTransactionTxId)
.subscribe((transactions: Transaction[]) => {
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
this.loadedConfirmedTxCount += transactions.length;
@@ -217,6 +217,10 @@ export class AddressComponent implements OnInit, OnDestroy {
(error) => {
this.isLoadingTransactions = false;
this.retryLoadMore = true;
// In the unlikely event of the txid wasn't found in the mempool anymore and we must reload the page.
if (error.status === 422) {
window.location.reload();
}
});
}

View File

@@ -38,7 +38,7 @@ export default class TxView implements TransactionStripped {
value: number;
feerate: number;
rate?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected' | 'fullrbf';
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
context?: 'projected' | 'actual';
scene?: BlockScene;
@@ -210,6 +210,7 @@ export default class TxView implements TransactionStripped {
case 'fullrbf':
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
case 'fresh':
case 'freshcpfp':
return auditColors.missing;
case 'added':
return auditColors.added;

View File

@@ -50,6 +50,7 @@
<td *ngSwitchCase="'missing'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
<td *ngSwitchCase="'sigop'"><span class="badge badge-warning" i18n="transaction.audit.sigop">High sigop count</span></td>
<td *ngSwitchCase="'fresh'"><span class="badge badge-warning" i18n="transaction.audit.recently-broadcasted">Recently broadcasted</span></td>
<td *ngSwitchCase="'freshcpfp'"><span class="badge badge-warning" i18n="transaction.audit.recently-cpfped">Recently CPFP'd</span></td>
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
<td *ngSwitchCase="'fullrbf'"><span class="badge badge-warning" i18n="transaction.audit.fullrbf">Full RBF</span></td>

View File

@@ -370,7 +370,11 @@ export class BlockComponent implements OnInit, OnDestroy {
tx.status = 'found';
} else {
if (isFresh[tx.txid]) {
tx.status = 'fresh';
if (tx.rate - (tx.fee / tx.vsize) >= 0.1) {
tx.status = 'freshcpfp';
} else {
tx.status = 'fresh';
}
} else if (isSigop[tx.txid]) {
tx.status = 'sigop';
} else if (isFullRbf[tx.txid]) {

View File

@@ -12,7 +12,7 @@
<div class="input-group-prepend">
<span class="input-group-text">{{ currency$ | async }}</span>
</div>
<input type="text" class="form-control" formControlName="fiat" (input)="transformInput('fiat')">
<input type="text" 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>
</div>
@@ -20,7 +20,7 @@
<div class="input-group-prepend">
<span class="input-group-text">BTC</span>
</div>
<input type="text" class="form-control" formControlName="bitcoin" (input)="transformInput('bitcoin')">
<input type="text" 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>
</div>
@@ -28,7 +28,7 @@
<div class="input-group-prepend">
<span class="input-group-text">sats</span>
</div>
<input type="text" class="form-control" formControlName="satoshis" (input)="transformInput('satoshis')">
<input type="text" class="form-control" formControlName="satoshis" (input)="transformInput('satoshis')" (click)="selectAll($event)">
<app-clipboard [button]="true" [text]="form.get('satoshis').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
</div>
</form>

View File

@@ -23,4 +23,8 @@
.sats {
font-size: 20px;
margin-left: 5px;
}
}
.row {
margin: auto;
}

View File

@@ -54,6 +54,9 @@ export class CalculatorComponent implements OnInit {
]).subscribe(([price, value]) => {
const rate = (value / price).toFixed(8);
const satsRate = Math.round(value / price * 100_000_000);
if (isNaN(value)) {
return;
}
this.form.get('bitcoin').setValue(rate, { emitEvent: false });
this.form.get('satoshis').setValue(satsRate, { emitEvent: false } );
});
@@ -63,6 +66,9 @@ export class CalculatorComponent implements OnInit {
this.form.get('bitcoin').valueChanges
]).subscribe(([price, value]) => {
const rate = parseFloat((value * price).toFixed(8));
if (isNaN(value)) {
return;
}
this.form.get('fiat').setValue(rate, { emitEvent: false } );
this.form.get('satoshis').setValue(Math.round(value * 100_000_000), { emitEvent: false } );
});
@@ -73,6 +79,9 @@ export class CalculatorComponent implements OnInit {
]).subscribe(([price, value]) => {
const rate = parseFloat((value / 100_000_000 * price).toFixed(8));
const bitcoinRate = (value / 100_000_000).toFixed(8);
if (isNaN(value)) {
return;
}
this.form.get('fiat').setValue(rate, { emitEvent: false } );
this.form.get('bitcoin').setValue(bitcoinRate, { emitEvent: false });
});
@@ -88,7 +97,16 @@ export class CalculatorComponent implements OnInit {
if (value === '.') {
value = '0';
}
const sanitizedValue = this.removeExtraDots(value);
let sanitizedValue = this.removeExtraDots(value);
if (name === 'bitcoin' && this.countDecimals(sanitizedValue) > 8) {
sanitizedValue = this.toFixedWithoutRounding(sanitizedValue, 8);
}
if (sanitizedValue === '') {
sanitizedValue = '0';
}
if (name === 'satoshis') {
sanitizedValue = parseFloat(sanitizedValue).toFixed(0);
}
formControl.setValue(sanitizedValue, {emitEvent: true});
}
@@ -100,4 +118,20 @@ export class CalculatorComponent implements OnInit {
const afterDotReplaced = afterDot.replace(/\./g, '');
return `${beforeDot}.${afterDotReplaced}`;
}
countDecimals(numberString: string): number {
const decimalPos = numberString.indexOf('.');
if (decimalPos === -1) return 0;
return numberString.length - decimalPos - 1;
}
toFixedWithoutRounding(numStr: string, fixed: number): string {
const re = new RegExp(`^-?\\d+(?:.\\d{0,${(fixed || -1)}})?`);
const result = numStr.match(re);
return result ? result[0] : numStr;
}
selectAll(event): void {
event.target.select();
}
}

View File

@@ -9,6 +9,7 @@
display: flex;
flex-direction: column;
justify-content: flex-start;
overflow: hidden;
--chain-height: 60px;
--clock-width: 300px;

View File

@@ -37,7 +37,7 @@ export class PoolComponent implements OnInit {
auditAvailable = false;
loadMoreSubject: BehaviorSubject<number> = new BehaviorSubject(this.blocks[0]?.height);
loadMoreSubject: BehaviorSubject<number> = new BehaviorSubject(this.blocks[this.blocks.length - 1]?.height);
constructor(
@Inject(LOCALE_ID) public locale: string,
@@ -91,7 +91,7 @@ export class PoolComponent implements OnInit {
if (this.slug === undefined) {
return [];
}
return this.apiService.getPoolBlocks$(this.slug, this.blocks[0]?.height);
return this.apiService.getPoolBlocks$(this.slug, this.blocks[this.blocks.length - 1]?.height);
}),
tap((newBlocks) => {
this.blocks = this.blocks.concat(newBlocks);
@@ -237,7 +237,7 @@ export class PoolComponent implements OnInit {
}
loadMore() {
this.loadMoreSubject.next(this.blocks[0]?.height);
this.loadMoreSubject.next(this.blocks[this.blocks.length - 1]?.height);
}
trackByBlock(index: number, block: BlockExtended) {

View File

@@ -2,7 +2,7 @@
<h1 class="float-left" i18n="page.rbf-replacements">RBF Replacements</h1>
<div *ngIf="isLoading" class="spinner-border ml-3" role="status"></div>
<div class="mode-toggle float-right" *ngIf="fullRbfEnabled">
<div class="mode-toggle float-right">
<form class="formRadioGroup">
<div class="btn-group btn-group-toggle" name="radioBasic">
<label class="btn btn-primary btn-sm" [class.active]="!fullRbf">

View File

@@ -17,7 +17,6 @@ export class RbfList implements OnInit, OnDestroy {
rbfTrees$: Observable<RbfTree[]>;
nextRbfSubject = new BehaviorSubject(null);
urlFragmentSubscription: Subscription;
fullRbfEnabled: boolean;
fullRbf: boolean;
isLoading = true;
@@ -27,9 +26,7 @@ export class RbfList implements OnInit, OnDestroy {
private apiService: ApiService,
public stateService: StateService,
private websocketService: WebsocketService,
) {
this.fullRbfEnabled = stateService.env.FULL_RBF_ENABLED;
}
) { }
ngOnInit(): void {
this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {

View File

@@ -19,6 +19,7 @@
<div class="container-buttons">
<app-confirmations
*ngIf="tx"
[chainTip]="latestBlock?.height"
[height]="tx?.status?.block_height"
[replaced]="replaced"

View File

@@ -379,7 +379,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
ancestors: tx.ancestors,
bestDescendant: tx.bestDescendant,
};
const hasRelatives = !!(tx.ancestors.length || tx.bestDescendant);
const hasRelatives = !!(tx.ancestors?.length || tx.bestDescendant);
this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && (Math.abs(tx.effectiveFeePerVsize - tx.feePerVsize) > 0.01));
} else {
this.fetchCpfp$.next(this.tx.txid);

View File

@@ -173,7 +173,8 @@ export interface TransactionStripped {
fee: number;
vsize: number;
value: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected' | 'fullrbf';
rate?: number; // effective fee rate
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
context?: 'projected' | 'actual';
}

View File

@@ -89,7 +89,7 @@ export interface TransactionStripped {
vsize: number;
value: number;
rate?: number; // effective fee rate
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected' | 'fullrbf';
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
context?: 'projected' | 'actual';
}

View File

@@ -21,7 +21,6 @@
</div>
<div class="box" *ngIf="!error">
<div class="row">
<div class="col-md">
<table class="table table-borderless table-striped table-fixed">
@@ -59,6 +58,9 @@
<td i18n="lightning.avg-distance" class="text-truncate">Avg channel distance</td>
<td class="direction-ltr">{{ avgDistance | amountShortener: 1 }} <span class="symbol">km</span> <span class="separator">·</span>{{ kmToMiles(avgDistance) | amountShortener: 1 }} <span class="symbol">mi</span></td>
</tr>
<tr *ngIf="!node.geolocation" class="d-none d-md-table-row">
<ng-container *ngTemplateOutlet="featurebits;context:{bits: node.featuresBits}"></ng-container>
</tr>
</tbody>
</table>
</div>
@@ -100,11 +102,50 @@
</td>
</ng-template>
</tr>
<tr *ngIf="node.geolocation && node.featuresBits">
<ng-container *ngTemplateOutlet="featurebits;context:{bits: node.featuresBits}"></ng-container>
</tr>
<tr *ngIf="!node.geolocation && node.featuresBits" class="d-table-row d-md-none">
<ng-container *ngTemplateOutlet="featurebits;context:{bits: node.featuresBits}"></ng-container>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<ng-template #featurebits let-bits="bits">
<td i18n="lightning.features" class="text-truncate label">Features</td>
<td class="d-flex justify-content-between">
<span class="text-truncate w-90">{{ bits }}</span>
<button type="button" class="btn btn-outline-info btn-xs" (click)="toggleFeatures()" i18n="transaction.details|Transaction Details">Details</button>
</td>
</ng-template>
<div class="box mt-2" *ngIf="!error && showFeatures">
<div class="row">
<div class="col-md">
<div class="mb-3">
<h5>Raw bits</h5>
<span class="text-wrap w-100"><small>{{ node.featuresBits }}</small></span>
</div>
<h5>Decoded</h5>
<table class="table table-borderless table-striped table-fixed">
<thead>
<th style="width: 13%">Bit</th>
<th>Name</th>
<th style="width: 25%; text-align: right">Required</th>
</thead>
<tbody>
<tr *ngFor="let feature of node.features">
<td style="width: 13%">{{ feature.bit }}</td>
<td>{{ feature.name }}</td>
<td style="width: 25%; text-align: right">{{ feature.is_required }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="input-group mt-3" *ngIf="!error && node.socketsObject.length">

View File

@@ -37,7 +37,7 @@ export class NodeComponent implements OnInit {
liquidityAd: ILiquidityAd;
tlvRecords: CustomRecord[];
avgChannelDistance$: Observable<number | null>;
showFeatures = false;
kmToMiles = kmToMiles;
constructor(
@@ -164,4 +164,9 @@ export class NodeComponent implements OnInit {
onLoadingEvent(e) {
this.channelListLoading = e;
}
toggleFeatures() {
this.showFeatures = !this.showFeatures;
return false;
}
}

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Transaction, Address, Outspend, Recent, Asset } from '../interfaces/electrs.interface';
import { StateService } from './state.service';
@@ -65,12 +65,12 @@ export class ElectrsApiService {
return this.httpClient.get<Address>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address);
}
getAddressTransactions$(address: string): Observable<Transaction[]> {
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs');
}
getAddressTransactionsFromHash$(address: string, txid: string): Observable<Transaction[]> {
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs/chain/' + txid);
getAddressTransactions$(address: string, txid?: string): Observable<Transaction[]> {
let params = new HttpParams();
if (txid) {
params = params.append('after_txid', txid);
}
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs', { params });
}
getAsset$(assetId: string): Observable<Asset> {

View File

@@ -45,7 +45,6 @@ export interface Env {
MAINNET_BLOCK_AUDIT_START_HEIGHT: number;
TESTNET_BLOCK_AUDIT_START_HEIGHT: number;
SIGNET_BLOCK_AUDIT_START_HEIGHT: number;
FULL_RBF_ENABLED: boolean;
HISTORICAL_PRICE: boolean;
}
@@ -76,7 +75,6 @@ const defaultEnv: Env = {
'MAINNET_BLOCK_AUDIT_START_HEIGHT': 0,
'TESTNET_BLOCK_AUDIT_START_HEIGHT': 0,
'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0,
'FULL_RBF_ENABLED': false,
'HISTORICAL_PRICE': true,
};

View File

@@ -5,12 +5,15 @@
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
</button>
</ng-template>
<ng-template [ngIf]="!confirmations && height != null">
<button type="button" class="btn btn-sm btn-success {{buttonClass}}" i18n="transaction.confirmed|Transaction confirmed state">Confirmed</button>
</ng-template>
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && replaced">
<button type="button" class="btn btn-sm btn-warning {{buttonClass}}" i18n="transaction.replaced|Transaction replaced state">Replaced</button>
</ng-template>
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced && removed">
<button type="button" class="btn btn-sm btn-warning {{buttonClass}}" i18n="transaction.audit.removed|Transaction removed state">Removed</button>
</ng-template>
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced && !removed">
<ng-template [ngIf]="!hideUnconfirmed && chainTip != null && !confirmations && !replaced && !removed">
<button type="button" class="btn btn-sm btn-danger {{buttonClass}}" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
</ng-template>

View File

@@ -45,8 +45,8 @@ $dropdown-link-hover-bg: #11131f;
$dropdown-link-active-color: #fff;
$dropdown-link-active-bg: #11131f;
@import "~bootstrap/scss/bootstrap";
@import '~tlite/tlite.css';
@import "bootstrap/scss/bootstrap";
@import 'tlite/tlite.css';
html, body {
height: 100%;
@@ -1164,3 +1164,10 @@ app-master-page, app-liquid-master-page, app-bisq-master-page {
app-global-footer {
margin-top: auto;
}
.btn-xs {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
line-height: 0.5;
border-radius: 0.2rem;
}