Merge branch 'master' into nymkappa/bugfix/mysql-inject
This commit is contained in:
commit
1a10acf8ce
@ -262,7 +262,8 @@ class ChannelsApi {
|
|||||||
let query = `
|
let query = `
|
||||||
SELECT COALESCE(node2.alias, SUBSTRING(node2_public_key, 0, 20)) AS alias, COALESCE(node2.public_key, node2_public_key) AS public_key,
|
SELECT COALESCE(node2.alias, SUBSTRING(node2_public_key, 0, 20)) AS alias, COALESCE(node2.public_key, node2_public_key) AS public_key,
|
||||||
channels.status, channels.node1_fee_rate,
|
channels.status, channels.node1_fee_rate,
|
||||||
channels.capacity, channels.short_id, channels.id, channels.closing_reason
|
channels.capacity, channels.short_id, channels.id, channels.closing_reason,
|
||||||
|
UNIX_TIMESTAMP(closing_date) as closing_date, UNIX_TIMESTAMP(channels.updated_at) as updated_at
|
||||||
FROM channels
|
FROM channels
|
||||||
LEFT JOIN nodes AS node2 ON node2.public_key = channels.node2_public_key
|
LEFT JOIN nodes AS node2 ON node2.public_key = channels.node2_public_key
|
||||||
WHERE node1_public_key = ? AND channels.status ${channelStatusFilter}
|
WHERE node1_public_key = ? AND channels.status ${channelStatusFilter}
|
||||||
@ -273,7 +274,8 @@ class ChannelsApi {
|
|||||||
query = `
|
query = `
|
||||||
SELECT COALESCE(node1.alias, SUBSTRING(node1_public_key, 0, 20)) AS alias, COALESCE(node1.public_key, node1_public_key) AS public_key,
|
SELECT COALESCE(node1.alias, SUBSTRING(node1_public_key, 0, 20)) AS alias, COALESCE(node1.public_key, node1_public_key) AS public_key,
|
||||||
channels.status, channels.node2_fee_rate,
|
channels.status, channels.node2_fee_rate,
|
||||||
channels.capacity, channels.short_id, channels.id, channels.closing_reason
|
channels.capacity, channels.short_id, channels.id, channels.closing_reason,
|
||||||
|
UNIX_TIMESTAMP(closing_date) as closing_date, UNIX_TIMESTAMP(channels.updated_at) as updated_at
|
||||||
FROM channels
|
FROM channels
|
||||||
LEFT JOIN nodes AS node1 ON node1.public_key = channels.node1_public_key
|
LEFT JOIN nodes AS node1 ON node1.public_key = channels.node1_public_key
|
||||||
WHERE node2_public_key = ? AND channels.status ${channelStatusFilter}
|
WHERE node2_public_key = ? AND channels.status ${channelStatusFilter}
|
||||||
@ -282,7 +284,15 @@ class ChannelsApi {
|
|||||||
|
|
||||||
let allChannels = channelsFromNode.concat(channelsToNode);
|
let allChannels = channelsFromNode.concat(channelsToNode);
|
||||||
allChannels.sort((a, b) => {
|
allChannels.sort((a, b) => {
|
||||||
return b.capacity - a.capacity;
|
if (status === 'closed') {
|
||||||
|
if (!b.closing_date && !a.closing_date) {
|
||||||
|
return (b.updated_at ?? 0) - (a.updated_at ?? 0);
|
||||||
|
} else {
|
||||||
|
return (b.closing_date ?? 0) - (a.closing_date ?? 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return b.capacity - a.capacity;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
@ -299,6 +309,7 @@ class ChannelsApi {
|
|||||||
channel = {
|
channel = {
|
||||||
status: row.status,
|
status: row.status,
|
||||||
closing_reason: row.closing_reason,
|
closing_reason: row.closing_reason,
|
||||||
|
closing_date: row.closing_date,
|
||||||
capacity: row.capacity ?? 0,
|
capacity: row.capacity ?? 0,
|
||||||
short_id: row.short_id,
|
short_id: row.short_id,
|
||||||
id: row.id,
|
id: row.id,
|
||||||
@ -527,6 +538,23 @@ class ChannelsApi {
|
|||||||
logger.err('$setChannelsInactive() error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$setChannelsInactive() error: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getLatestChannelUpdateForNode(publicKey: string): Promise<number> {
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
SELECT MAX(UNIX_TIMESTAMP(updated_at)) as updated_at
|
||||||
|
FROM channels
|
||||||
|
WHERE node1_public_key = ?
|
||||||
|
`;
|
||||||
|
const [rows]: any[] = await DB.query(query, [publicKey]);
|
||||||
|
if (rows.length > 0) {
|
||||||
|
return rows[0].updated_at;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Can't getLatestChannelUpdateForNode for ${publicKey}. Reason ${e instanceof Error ? e.message : e}`);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ChannelsApi();
|
export default new ChannelsApi();
|
||||||
|
@ -63,6 +63,9 @@ class NetworkSyncService {
|
|||||||
let deletedSockets = 0;
|
let deletedSockets = 0;
|
||||||
const graphNodesPubkeys: string[] = [];
|
const graphNodesPubkeys: string[] = [];
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
|
const latestUpdated = await channelsApi.$getLatestChannelUpdateForNode(node.pub_key);
|
||||||
|
node.last_update = Math.max(node.last_update, latestUpdated);
|
||||||
|
|
||||||
await nodesApi.$saveNode(node);
|
await nodesApi.$saveNode(node);
|
||||||
graphNodesPubkeys.push(node.pub_key);
|
graphNodesPubkeys.push(node.pub_key);
|
||||||
++progress;
|
++progress;
|
||||||
|
@ -7,7 +7,13 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button [disabled]="isSearching" type="submit" class="btn btn-block btn-primary"><fa-icon [icon]="['fas', 'search']" [fixedWidth]="true" i18n-title="search-form.search-title" title="Search"></fa-icon></button>
|
<button [disabled]="isSearching" type="submit" class="btn btn-block btn-primary">
|
||||||
|
<fa-icon *ngIf="!(isTypeaheading$ | async) else searchLoading" [icon]="['fas', 'search']" [fixedWidth]="true" i18n-title="search-form.search-title" title="Search"></fa-icon>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<ng-template #searchLoading>
|
||||||
|
<div class="spinner-border spinner-border-sm text-light" role="status" aria-hidden="true" (click)="searchForm.valid && search()"></div>
|
||||||
|
</ng-template>
|
||||||
|
@ -50,3 +50,9 @@ form {
|
|||||||
width: 100px;
|
width: 100px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spinner-border {
|
||||||
|
vertical-align: text-top;
|
||||||
|
margin-top: 1px;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
@ -3,8 +3,8 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { AssetsService } from 'src/app/services/assets.service';
|
import { AssetsService } from 'src/app/services/assets.service';
|
||||||
import { StateService } from 'src/app/services/state.service';
|
import { StateService } from 'src/app/services/state.service';
|
||||||
import { Observable, of, Subject, merge, zip } from 'rxjs';
|
import { Observable, of, Subject, zip, BehaviorSubject } from 'rxjs';
|
||||||
import { debounceTime, distinctUntilChanged, switchMap, filter, catchError, map } from 'rxjs/operators';
|
import { debounceTime, distinctUntilChanged, switchMap, catchError, map } from 'rxjs/operators';
|
||||||
import { ElectrsApiService } from 'src/app/services/electrs-api.service';
|
import { ElectrsApiService } from 'src/app/services/electrs-api.service';
|
||||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
||||||
import { ApiService } from 'src/app/services/api.service';
|
import { ApiService } from 'src/app/services/api.service';
|
||||||
@ -20,13 +20,14 @@ export class SearchFormComponent implements OnInit {
|
|||||||
network = '';
|
network = '';
|
||||||
assets: object = {};
|
assets: object = {};
|
||||||
isSearching = false;
|
isSearching = false;
|
||||||
|
isTypeaheading$ = new BehaviorSubject<boolean>(false);
|
||||||
typeAhead$: Observable<any>;
|
typeAhead$: Observable<any>;
|
||||||
searchForm: FormGroup;
|
searchForm: FormGroup;
|
||||||
|
|
||||||
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/;
|
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/;
|
||||||
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
|
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
|
||||||
regexTransaction = /^([a-fA-F0-9]{64}):?(\d+)?$/;
|
regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
|
||||||
regexBlockheight = /^[0-9]+$/;
|
regexBlockheight = /^[0-9]{1,9}$/;
|
||||||
focus$ = new Subject<string>();
|
focus$ = new Subject<string>();
|
||||||
click$ = new Subject<string>();
|
click$ = new Subject<string>();
|
||||||
|
|
||||||
@ -68,7 +69,7 @@ export class SearchFormComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
return text.trim();
|
return text.trim();
|
||||||
}),
|
}),
|
||||||
debounceTime(250),
|
debounceTime(200),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
switchMap((text) => {
|
switchMap((text) => {
|
||||||
if (!text.length) {
|
if (!text.length) {
|
||||||
@ -80,6 +81,7 @@ export class SearchFormComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
this.isTypeaheading$.next(true);
|
||||||
if (!this.stateService.env.LIGHTNING) {
|
if (!this.stateService.env.LIGHTNING) {
|
||||||
return zip(
|
return zip(
|
||||||
this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))),
|
this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))),
|
||||||
@ -95,6 +97,7 @@ export class SearchFormComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
map((result: any[]) => {
|
map((result: any[]) => {
|
||||||
|
this.isTypeaheading$.next(false);
|
||||||
if (this.network === 'bisq') {
|
if (this.network === 'bisq') {
|
||||||
return result[0].map((address: string) => 'B' + address);
|
return result[0].map((address: string) => 'B' + address);
|
||||||
}
|
}
|
||||||
@ -153,6 +156,7 @@ export class SearchFormComponent implements OnInit {
|
|||||||
this.navigate('/tx/', matches[0]);
|
this.navigate('/tx/', matches[0]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
this.searchResults.searchButtonClick();
|
||||||
this.isSearching = false;
|
this.isSearching = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,13 @@ export class SearchResultsComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
searchButtonClick() {
|
||||||
|
if (this.resultsFlattened[this.activeIdx]) {
|
||||||
|
this.selectedResult.emit(this.resultsFlattened[this.activeIdx]);
|
||||||
|
this.results = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleKeyDown(event: KeyboardEvent) {
|
handleKeyDown(event: KeyboardEvent) {
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
|
@ -65,14 +65,18 @@
|
|||||||
|
|
||||||
<ng-container *ngIf="transactions$ | async as transactions">
|
<ng-container *ngIf="transactions$ | async as transactions">
|
||||||
<ng-template [ngIf]="transactions[0]">
|
<ng-template [ngIf]="transactions[0]">
|
||||||
<h3>Opening transaction</h3>
|
<div class="d-flex">
|
||||||
<app-transactions-list [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list>
|
<h3>Opening transaction</h3>
|
||||||
|
<button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList1.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button>
|
||||||
|
</div>
|
||||||
|
<app-transactions-list #txList1 [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template [ngIf]="transactions[1]">
|
<ng-template [ngIf]="transactions[1]">
|
||||||
<div class="closing-header">
|
<div class="closing-header d-flex">
|
||||||
<h3 style="margin: 0;">Closing transaction</h3> <app-closing-type [type]="channel.closing_reason"></app-closing-type>
|
<h3 style="margin: 0;">Closing transaction</h3> <app-closing-type [type]="channel.closing_reason"></app-closing-type>
|
||||||
|
<button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList2.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button>
|
||||||
</div>
|
</div>
|
||||||
<app-transactions-list [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list>
|
<app-transactions-list #txList2 [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@ -45,19 +45,29 @@ app-fiat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.closing-header {
|
.closing-header {
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 0rem;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.closing-header {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
app-closing-type {
|
||||||
|
flex-basis: 100%;
|
||||||
|
order: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.loading-spinner {
|
.loading-spinner {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 400px;
|
top: 400px;
|
||||||
@ -68,3 +78,8 @@ app-fiat {
|
|||||||
top: 450px;
|
top: 450px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.details-button {
|
||||||
|
align-self: center;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
@ -22,30 +22,30 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<table class="table table-borderless table-striped">
|
<table class="table table-borderless table-striped table-fixed">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="lightning.active-capacity">Active capacity</td>
|
<td i18n="lightning.active-capacity" class="text-truncate label">Active capacity</td>
|
||||||
<td>
|
<td>
|
||||||
<app-sats [satoshis]="node.capacity"></app-sats>
|
<app-sats [satoshis]="node.capacity"></app-sats>
|
||||||
<app-fiat [value]="node.capacity" digitsInfo="1.0-0"></app-fiat>
|
<app-fiat [value]="node.capacity" digitsInfo="1.0-0"></app-fiat>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="lightning.active-channels">Active channels</td>
|
<td i18n="lightning.active-channels" class="text-truncate label">Active channels</td>
|
||||||
<td>
|
<td>
|
||||||
{{ node.active_channel_count }}
|
{{ node.active_channel_count }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="lightning.active-channels-avg">Average channel size</td>
|
<td i18n="lightning.active-channels-avg" class="text-wrap label">Average channel size</td>
|
||||||
<td>
|
<td>
|
||||||
<app-sats [satoshis]="node.avgCapacity"></app-sats>
|
<app-sats [satoshis]="node.avgCapacity"></app-sats>
|
||||||
<app-fiat [value]="node.avgCapacity" digitsInfo="1.0-0"></app-fiat>
|
<app-fiat [value]="node.avgCapacity" digitsInfo="1.0-0"></app-fiat>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="node.geolocation">
|
<tr *ngIf="node.geolocation">
|
||||||
<td i18n="location">Location</td>
|
<td i18n="location" class="text-truncate">Location</td>
|
||||||
<td>
|
<td>
|
||||||
<app-geolocation [data]="node.geolocation" [type]="'node'"></app-geolocation>
|
<app-geolocation [data]="node.geolocation" [type]="'node'"></app-geolocation>
|
||||||
</td>
|
</td>
|
||||||
@ -55,30 +55,30 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="w-100 d-block d-md-none"></div>
|
<div class="w-100 d-block d-md-none"></div>
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<table class="table table-borderless table-striped">
|
<table class="table table-borderless table-striped table-fixed">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="address.total-received">First seen</td>
|
<td i18n="address.total-received" class="text-truncate label">First seen</td>
|
||||||
<td>
|
<td>
|
||||||
<app-timestamp [unixTime]="node.first_seen"></app-timestamp>
|
<app-timestamp [unixTime]="node.first_seen"></app-timestamp>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="address.total-sent">Last update</td>
|
<td i18n="address.total-sent" class="text-truncate label">Last update</td>
|
||||||
<td>
|
<td>
|
||||||
<app-timestamp [unixTime]="node.updated_at"></app-timestamp>
|
<app-timestamp [unixTime]="node.updated_at"></app-timestamp>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="address.balance">Color</td>
|
<td i18n="address.balance" class="text-truncate label">Color</td>
|
||||||
<td>
|
<td>
|
||||||
<div [ngStyle]="{'color': node.color}">{{ node.color }}</div>
|
<div [ngStyle]="{'color': node.color}">{{ node.color }}</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="node.country">
|
<tr *ngIf="node.country">
|
||||||
<td i18n="isp">ISP</td>
|
<td i18n="isp" class="text-truncate label">ISP</td>
|
||||||
<td>
|
<td>
|
||||||
<a [routerLink]="['/lightning/nodes/isp' | relativeUrl, node.as_number]">
|
<a class="d-block text-wrap" [routerLink]="['/lightning/nodes/isp' | relativeUrl, node.as_number]">
|
||||||
{{ node.as_organization }} [ASN {{node.as_number}}]
|
{{ node.as_organization }} [ASN {{node.as_number}}]
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
@ -77,3 +77,10 @@ app-fiat {
|
|||||||
left: calc(50% - 15px);
|
left: calc(50% - 15px);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
width: 50%;
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1 @@
|
|||||||
<span [innerHTML]="formattedLocation"></span>
|
<span class="d-block text-truncate" [innerHTML]="formattedLocation"></span>
|
Loading…
x
Reference in New Issue
Block a user