Merge branch 'master' into nymkappa/feature/align-dashboards
This commit is contained in:
commit
ab6219a828
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -9,7 +9,7 @@ jobs:
|
|||||||
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: ["16.16.0", "18.5.0"]
|
node: ["16.16.0", "18.14.1", "19.6.1"]
|
||||||
flavor: ["dev", "prod"]
|
flavor: ["dev", "prod"]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
@ -55,7 +55,7 @@ jobs:
|
|||||||
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: ["16.15.0", "18.5.0"]
|
node: ["16.16.0", "18.14.1", "19.6.1"]
|
||||||
flavor: ["dev", "prod"]
|
flavor: ["dev", "prod"]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
|
@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
|||||||
import { RowDataPacket } from 'mysql2';
|
import { RowDataPacket } from 'mysql2';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 53;
|
private static currentVersion = 54;
|
||||||
private queryTimeout = 3600_000;
|
private queryTimeout = 3600_000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
private uniqueLogs: string[] = [];
|
private uniqueLogs: string[] = [];
|
||||||
@ -464,7 +464,7 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('DROP TABLE IF EXISTS `transactions`');
|
await this.$executeQuery('DROP TABLE IF EXISTS `transactions`');
|
||||||
await this.$executeQuery('DROP TABLE IF EXISTS `cpfp_clusters`');
|
await this.$executeQuery('DROP TABLE IF EXISTS `cpfp_clusters`');
|
||||||
await this.updateToSchemaVersion(52);
|
await this.updateToSchemaVersion(52);
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
logger.warn('' + (e instanceof Error ? e.message : e));
|
logger.warn('' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -473,6 +473,16 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE statistics MODIFY mempool_byte_weight bigint(20) UNSIGNED NOT NULL');
|
await this.$executeQuery('ALTER TABLE statistics MODIFY mempool_byte_weight bigint(20) UNSIGNED NOT NULL');
|
||||||
await this.updateToSchemaVersion(53);
|
await this.updateToSchemaVersion(53);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 54) {
|
||||||
|
this.uniqueLog(logger.notice, `'prices' table has been truncated`);
|
||||||
|
await this.$executeQuery(`TRUNCATE prices`);
|
||||||
|
if (isBitcoin === true) {
|
||||||
|
this.uniqueLog(logger.notice, `'blocks_prices' table has been truncated`);
|
||||||
|
await this.$executeQuery(`TRUNCATE blocks_prices`);
|
||||||
|
}
|
||||||
|
await this.updateToSchemaVersion(54);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -38,7 +38,16 @@ class MiningRoutes {
|
|||||||
|
|
||||||
private async $getHistoricalPrice(req: Request, res: Response): Promise<void> {
|
private async $getHistoricalPrice(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
res.status(200).send(await PricesRepository.$getHistoricalPrice());
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||||
|
if (req.query.timestamp) {
|
||||||
|
res.status(200).send(await PricesRepository.$getNearestHistoricalPrice(
|
||||||
|
parseInt(<string>req.query.timestamp ?? 0, 10)
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
res.status(200).send(await PricesRepository.$getHistoricalPrices());
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
|
@ -521,7 +521,7 @@ class BlocksRepository {
|
|||||||
CAST(AVG(blocks.height) as INT) as avgHeight,
|
CAST(AVG(blocks.height) as INT) as avgHeight,
|
||||||
CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
|
CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
|
||||||
CAST(AVG(fees) as INT) as avgFees,
|
CAST(AVG(fees) as INT) as avgFees,
|
||||||
prices.*
|
prices.USD
|
||||||
FROM blocks
|
FROM blocks
|
||||||
JOIN blocks_prices on blocks_prices.height = blocks.height
|
JOIN blocks_prices on blocks_prices.height = blocks.height
|
||||||
JOIN prices on prices.id = blocks_prices.price_id
|
JOIN prices on prices.id = blocks_prices.price_id
|
||||||
@ -550,7 +550,7 @@ class BlocksRepository {
|
|||||||
CAST(AVG(blocks.height) as INT) as avgHeight,
|
CAST(AVG(blocks.height) as INT) as avgHeight,
|
||||||
CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
|
CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
|
||||||
CAST(AVG(reward) as INT) as avgRewards,
|
CAST(AVG(reward) as INT) as avgRewards,
|
||||||
prices.*
|
prices.USD
|
||||||
FROM blocks
|
FROM blocks
|
||||||
JOIN blocks_prices on blocks_prices.height = blocks.height
|
JOIN blocks_prices on blocks_prices.height = blocks.height
|
||||||
JOIN prices on prices.id = blocks_prices.price_id
|
JOIN prices on prices.id = blocks_prices.price_id
|
||||||
|
@ -28,6 +28,16 @@ export interface Conversion {
|
|||||||
exchangeRates: ExchangeRates;
|
exchangeRates: ExchangeRates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const MAX_PRICES = {
|
||||||
|
USD: 100000000,
|
||||||
|
EUR: 100000000,
|
||||||
|
GBP: 100000000,
|
||||||
|
CAD: 100000000,
|
||||||
|
CHF: 100000000,
|
||||||
|
AUD: 100000000,
|
||||||
|
JPY: 10000000000,
|
||||||
|
};
|
||||||
|
|
||||||
class PricesRepository {
|
class PricesRepository {
|
||||||
public async $savePrices(time: number, prices: IConversionRates): Promise<void> {
|
public async $savePrices(time: number, prices: IConversionRates): Promise<void> {
|
||||||
if (prices.USD === 0) {
|
if (prices.USD === 0) {
|
||||||
@ -36,6 +46,14 @@ class PricesRepository {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sanity check
|
||||||
|
for (const currency of Object.keys(prices)) {
|
||||||
|
if (prices[currency] < -1 || prices[currency] > MAX_PRICES[currency]) { // We use -1 to mark a "missing data, so it's a valid entry"
|
||||||
|
logger.info(`Ignore BTC${currency} price of ${prices[currency]}`);
|
||||||
|
prices[currency] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await DB.query(`
|
await DB.query(`
|
||||||
INSERT INTO prices(time, USD, EUR, GBP, CAD, CHF, AUD, JPY)
|
INSERT INTO prices(time, USD, EUR, GBP, CAD, CHF, AUD, JPY)
|
||||||
@ -86,9 +104,48 @@ class PricesRepository {
|
|||||||
return rates[0];
|
return rates[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getHistoricalPrice(): Promise<Conversion | null> {
|
public async $getNearestHistoricalPrice(timestamp: number | undefined): Promise<Conversion | null> {
|
||||||
try {
|
try {
|
||||||
const [rates]: any[] = await DB.query(`SELECT *, UNIX_TIMESTAMP(time) as time FROM prices ORDER BY time DESC`);
|
const [rates]: any[] = await DB.query(`
|
||||||
|
SELECT *, UNIX_TIMESTAMP(time) AS time
|
||||||
|
FROM prices
|
||||||
|
WHERE UNIX_TIMESTAMP(time) < ?
|
||||||
|
ORDER BY time DESC
|
||||||
|
LIMIT 1`,
|
||||||
|
[timestamp]
|
||||||
|
);
|
||||||
|
if (!rates) {
|
||||||
|
throw Error(`Cannot get single historical price from the database`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute fiat exchange rates
|
||||||
|
const latestPrice = await this.$getLatestConversionRates();
|
||||||
|
const exchangeRates: ExchangeRates = {
|
||||||
|
USDEUR: Math.round(latestPrice.EUR / latestPrice.USD * 100) / 100,
|
||||||
|
USDGBP: Math.round(latestPrice.GBP / latestPrice.USD * 100) / 100,
|
||||||
|
USDCAD: Math.round(latestPrice.CAD / latestPrice.USD * 100) / 100,
|
||||||
|
USDCHF: Math.round(latestPrice.CHF / latestPrice.USD * 100) / 100,
|
||||||
|
USDAUD: Math.round(latestPrice.AUD / latestPrice.USD * 100) / 100,
|
||||||
|
USDJPY: Math.round(latestPrice.JPY / latestPrice.USD * 100) / 100,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
prices: rates,
|
||||||
|
exchangeRates: exchangeRates
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot fetch single historical prices from the db. Reason ${e instanceof Error ? e.message : e}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getHistoricalPrices(): Promise<Conversion | null> {
|
||||||
|
try {
|
||||||
|
const [rates]: any[] = await DB.query(`
|
||||||
|
SELECT *, UNIX_TIMESTAMP(time) AS time
|
||||||
|
FROM prices
|
||||||
|
ORDER BY time DESC
|
||||||
|
`);
|
||||||
if (!rates) {
|
if (!rates) {
|
||||||
throw Error(`Cannot get average historical price from the database`);
|
throw Error(`Cannot get average historical price from the database`);
|
||||||
}
|
}
|
||||||
@ -109,7 +166,7 @@ class PricesRepository {
|
|||||||
exchangeRates: exchangeRates
|
exchangeRates: exchangeRates
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Cannot fetch averaged historical prices from the db. Reason ${e instanceof Error ? e.message : e}`);
|
logger.err(`Cannot fetch historical prices from the db. Reason ${e instanceof Error ? e.message : e}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import path from 'path';
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { IConversionRates } from '../mempool.interfaces';
|
import { IConversionRates } from '../mempool.interfaces';
|
||||||
import PricesRepository from '../repositories/PricesRepository';
|
import PricesRepository, { MAX_PRICES } from '../repositories/PricesRepository';
|
||||||
import BitfinexApi from './price-feeds/bitfinex-api';
|
import BitfinexApi from './price-feeds/bitfinex-api';
|
||||||
import BitflyerApi from './price-feeds/bitflyer-api';
|
import BitflyerApi from './price-feeds/bitflyer-api';
|
||||||
import CoinbaseApi from './price-feeds/coinbase-api';
|
import CoinbaseApi from './price-feeds/coinbase-api';
|
||||||
@ -46,13 +46,13 @@ class PriceUpdater {
|
|||||||
|
|
||||||
public getEmptyPricesObj(): IConversionRates {
|
public getEmptyPricesObj(): IConversionRates {
|
||||||
return {
|
return {
|
||||||
USD: 0,
|
USD: -1,
|
||||||
EUR: 0,
|
EUR: -1,
|
||||||
GBP: 0,
|
GBP: -1,
|
||||||
CAD: 0,
|
CAD: -1,
|
||||||
CHF: 0,
|
CHF: -1,
|
||||||
AUD: 0,
|
AUD: -1,
|
||||||
JPY: 0,
|
JPY: -1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +115,7 @@ class PriceUpdater {
|
|||||||
if (feed.currencies.includes(currency)) {
|
if (feed.currencies.includes(currency)) {
|
||||||
try {
|
try {
|
||||||
const price = await feed.$fetchPrice(currency);
|
const price = await feed.$fetchPrice(currency);
|
||||||
if (price > 0) {
|
if (price > -1 && price < MAX_PRICES[currency]) {
|
||||||
prices.push(price);
|
prices.push(price);
|
||||||
}
|
}
|
||||||
logger.debug(`${feed.name} BTC/${currency} price: ${price}`, logger.tags.mining);
|
logger.debug(`${feed.name} BTC/${currency} price: ${price}`, logger.tags.mining);
|
||||||
@ -239,7 +239,7 @@ class PriceUpdater {
|
|||||||
|
|
||||||
for (const currency of this.currencies) {
|
for (const currency of this.currencies) {
|
||||||
const price = historicalEntry[time][currency];
|
const price = historicalEntry[time][currency];
|
||||||
if (price > 0) {
|
if (price > -1 && price < MAX_PRICES[currency]) {
|
||||||
grouped[time][currency].push(typeof price === 'string' ? parseInt(price, 10) : price);
|
grouped[time][currency].push(typeof price === 'string' ? parseInt(price, 10) : price);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,6 +145,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-translators .wrapper {
|
||||||
|
a img {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.copyright {
|
.copyright {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
max-width: 620px;
|
max-width: 620px;
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
{{ addPlus && satoshis >= 0 ? '+' : '' }}
|
{{ addPlus && satoshis >= 0 ? '+' : '' }}
|
||||||
{{
|
{{
|
||||||
(
|
(
|
||||||
(blockConversion.price[currency] > 0 ? blockConversion.price[currency] : null) ??
|
(blockConversion.price[currency] >= 0 ? blockConversion.price[currency] : null) ??
|
||||||
(blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency]) ?? 0
|
(blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency]) ?? 0
|
||||||
) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency
|
) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency
|
||||||
}}
|
}}
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||||
import { EChartsOption, graphic } from 'echarts';
|
import { EChartsOption, graphic } from 'echarts';
|
||||||
import { Observable, Subscription } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { download, formatterXAxis } from '../../shared/graphs.utils';
|
import { download, formatterXAxis } from '../../shared/graphs.utils';
|
||||||
import { StateService } from '../../services/state.service';
|
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
import { MiningService } from '../../services/mining.service';
|
import { MiningService } from '../../services/mining.service';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
|
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
|
||||||
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
|
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
|
||||||
import { fiatCurrencies } from '../../app.constants';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-block-fees-graph',
|
selector: 'app-block-fees-graph',
|
||||||
@ -47,7 +45,6 @@ export class BlockFeesGraphComponent implements OnInit {
|
|||||||
timespan = '';
|
timespan = '';
|
||||||
chartInstance: any = undefined;
|
chartInstance: any = undefined;
|
||||||
|
|
||||||
currencySubscription: Subscription;
|
|
||||||
currency: string;
|
currency: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -57,22 +54,14 @@ export class BlockFeesGraphComponent implements OnInit {
|
|||||||
private formBuilder: UntypedFormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private miningService: MiningService,
|
private miningService: MiningService,
|
||||||
private stateService: StateService,
|
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private fiatShortenerPipe: FiatShortenerPipe,
|
private fiatShortenerPipe: FiatShortenerPipe,
|
||||||
private fiatCurrencyPipe: FiatCurrencyPipe,
|
private fiatCurrencyPipe: FiatCurrencyPipe,
|
||||||
) {
|
) {
|
||||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
|
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
|
||||||
this.radioGroupForm.controls.dateSpan.setValue('1y');
|
this.radioGroupForm.controls.dateSpan.setValue('1y');
|
||||||
|
|
||||||
this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
|
|
||||||
if (fiat && fiatCurrencies[fiat]?.indexed) {
|
|
||||||
this.currency = fiat;
|
|
||||||
} else {
|
|
||||||
this.currency = 'USD';
|
this.currency = 'USD';
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.seoService.setTitle($localize`:@@6c453b11fd7bd159ae30bc381f367bc736d86909:Block Fees`);
|
this.seoService.setTitle($localize`:@@6c453b11fd7bd159ae30bc381f367bc736d86909:Block Fees`);
|
||||||
|
@ -10,5 +10,6 @@
|
|||||||
[cursorPosition]="tooltipPosition"
|
[cursorPosition]="tooltipPosition"
|
||||||
[clickable]="!!selectedTx"
|
[clickable]="!!selectedTx"
|
||||||
[auditEnabled]="auditHighlighting"
|
[auditEnabled]="auditHighlighting"
|
||||||
|
[blockConversion]="blockConversion"
|
||||||
></app-block-overview-tooltip>
|
></app-block-overview-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,6 +5,7 @@ import BlockScene from './block-scene';
|
|||||||
import TxSprite from './tx-sprite';
|
import TxSprite from './tx-sprite';
|
||||||
import TxView from './tx-view';
|
import TxView from './tx-view';
|
||||||
import { Position } from './sprite-types';
|
import { Position } from './sprite-types';
|
||||||
|
import { Price } from 'src/app/services/price.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-block-overview-graph',
|
selector: 'app-block-overview-graph',
|
||||||
@ -21,6 +22,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
@Input() mirrorTxid: string | void;
|
@Input() mirrorTxid: string | void;
|
||||||
@Input() unavailable: boolean = false;
|
@Input() unavailable: boolean = false;
|
||||||
@Input() auditHighlighting: boolean = false;
|
@Input() auditHighlighting: boolean = false;
|
||||||
|
@Input() blockConversion: Price;
|
||||||
@Output() txClickEvent = new EventEmitter<TransactionStripped>();
|
@Output() txClickEvent = new EventEmitter<TransactionStripped>();
|
||||||
@Output() txHoverEvent = new EventEmitter<string>();
|
@Output() txHoverEvent = new EventEmitter<string>();
|
||||||
@Output() readyEvent = new EventEmitter();
|
@Output() readyEvent = new EventEmitter();
|
||||||
|
@ -16,11 +16,11 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-width" i18n="dashboard.latest-transactions.amount">Amount</td>
|
<td class="td-width" i18n="dashboard.latest-transactions.amount">Amount</td>
|
||||||
<td><app-amount [satoshis]="value"></app-amount></td>
|
<td><app-amount [blockConversion]="blockConversion" [satoshis]="value"></app-amount></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||||
<td>{{ fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [value]="fee"></app-fiat></span></td>
|
<td>{{ fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [blockConversion]="blockConversion" [value]="fee"></app-fiat></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-width" i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
|
<td class="td-width" i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Component, ElementRef, ViewChild, Input, OnChanges, ChangeDetectionStrategy } from '@angular/core';
|
import { Component, ElementRef, ViewChild, Input, OnChanges, ChangeDetectionStrategy } from '@angular/core';
|
||||||
import { TransactionStripped } from '../../interfaces/websocket.interface';
|
import { TransactionStripped } from '../../interfaces/websocket.interface';
|
||||||
import { Position } from '../../components/block-overview-graph/sprite-types.js';
|
import { Position } from '../../components/block-overview-graph/sprite-types.js';
|
||||||
|
import { Price } from 'src/app/services/price.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-block-overview-tooltip',
|
selector: 'app-block-overview-tooltip',
|
||||||
@ -12,6 +13,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
|
|||||||
@Input() cursorPosition: Position;
|
@Input() cursorPosition: Position;
|
||||||
@Input() clickable: boolean;
|
@Input() clickable: boolean;
|
||||||
@Input() auditEnabled: boolean = false;
|
@Input() auditEnabled: boolean = false;
|
||||||
|
@Input() blockConversion: Price;
|
||||||
|
|
||||||
txid = '';
|
txid = '';
|
||||||
fee = 0;
|
fee = 0;
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||||
import { EChartsOption, graphic } from 'echarts';
|
import { EChartsOption, graphic } from 'echarts';
|
||||||
import { Observable, Subscription } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
import { download, formatterXAxis } from '../../shared/graphs.utils';
|
||||||
import { MiningService } from '../../services/mining.service';
|
import { MiningService } from '../../services/mining.service';
|
||||||
import { StateService } from '../../services/state.service';
|
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
|
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
|
||||||
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
|
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
|
||||||
import { fiatCurrencies } from '../../app.constants';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-block-rewards-graph',
|
selector: 'app-block-rewards-graph',
|
||||||
@ -47,7 +45,6 @@ export class BlockRewardsGraphComponent implements OnInit {
|
|||||||
timespan = '';
|
timespan = '';
|
||||||
chartInstance: any = undefined;
|
chartInstance: any = undefined;
|
||||||
|
|
||||||
currencySubscription: Subscription;
|
|
||||||
currency: string;
|
currency: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -56,20 +53,13 @@ export class BlockRewardsGraphComponent implements OnInit {
|
|||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private formBuilder: UntypedFormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private miningService: MiningService,
|
private miningService: MiningService,
|
||||||
private stateService: StateService,
|
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private fiatShortenerPipe: FiatShortenerPipe,
|
private fiatShortenerPipe: FiatShortenerPipe,
|
||||||
private fiatCurrencyPipe: FiatCurrencyPipe,
|
private fiatCurrencyPipe: FiatCurrencyPipe,
|
||||||
) {
|
) {
|
||||||
this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
|
|
||||||
if (fiat && fiatCurrencies[fiat]?.indexed) {
|
|
||||||
this.currency = fiat;
|
|
||||||
} else {
|
|
||||||
this.currency = 'USD';
|
this.currency = 'USD';
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.seoService.setTitle($localize`:@@8ba8fe810458280a83df7fdf4c614dfc1a826445:Block Rewards`);
|
this.seoService.setTitle($localize`:@@8ba8fe810458280a83df7fdf4c614dfc1a826445:Block Rewards`);
|
||||||
|
@ -108,6 +108,7 @@
|
|||||||
[blockLimit]="stateService.blockVSize"
|
[blockLimit]="stateService.blockVSize"
|
||||||
[orientation]="'top'"
|
[orientation]="'top'"
|
||||||
[flip]="false"
|
[flip]="false"
|
||||||
|
[blockConversion]="blockConversion"
|
||||||
(txClickEvent)="onTxClick($event)"
|
(txClickEvent)="onTxClick($event)"
|
||||||
></app-block-overview-graph>
|
></app-block-overview-graph>
|
||||||
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
|
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
|
||||||
|
@ -443,9 +443,9 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
this.priceSubscription = block$.pipe(
|
this.priceSubscription = block$.pipe(
|
||||||
switchMap((block) => {
|
switchMap((block) => {
|
||||||
return this.priceService.getPrices().pipe(
|
return this.priceService.getBlockPrice$(block.timestamp).pipe(
|
||||||
tap(() => {
|
tap((price) => {
|
||||||
this.blockConversion = this.priceService.getPriceForTimestamp(block.timestamp);
|
this.blockConversion = price;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@ -471,6 +471,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.auditSubscription?.unsubscribe();
|
this.auditSubscription?.unsubscribe();
|
||||||
this.unsubscribeNextBlockSubscriptions();
|
this.unsubscribeNextBlockSubscriptions();
|
||||||
this.childChangeSubscription?.unsubscribe();
|
this.childChangeSubscription?.unsubscribe();
|
||||||
|
this.priceSubscription?.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
unsubscribeNextBlockSubscriptions() {
|
unsubscribeNextBlockSubscriptions() {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div class="holder" [ngStyle]="{'width': size, 'height': size}">
|
<div class="holder" [ngStyle]="{'width': size, 'height': size}">
|
||||||
<img *ngIf="imageUrl" [src]="imageUrl">
|
<img *ngIf="imageUrl" [src]="imageUrl">
|
||||||
<canvas #canvas></canvas>
|
<canvas #canvas [style]="{'border': border + 'px solid white'}"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,6 +12,7 @@ export class QrcodeComponent implements AfterViewInit {
|
|||||||
@Input() data: string;
|
@Input() data: string;
|
||||||
@Input() size = 125;
|
@Input() size = 125;
|
||||||
@Input() imageUrl: string;
|
@Input() imageUrl: string;
|
||||||
|
@Input() border = 0;
|
||||||
@ViewChild('canvas') canvas: ElementRef;
|
@ViewChild('canvas') canvas: ElementRef;
|
||||||
|
|
||||||
qrcodeObject: any;
|
qrcodeObject: any;
|
||||||
|
@ -123,7 +123,7 @@ export class StartComponent implements OnInit, OnDestroy {
|
|||||||
this.minScrollWidth = this.firstPageWidth + (this.pageWidth * 2);
|
this.minScrollWidth = this.firstPageWidth + (this.pageWidth * 2);
|
||||||
|
|
||||||
if (firstVisibleBlock != null) {
|
if (firstVisibleBlock != null) {
|
||||||
this.scrollToBlock(firstVisibleBlock, offset);
|
this.scrollToBlock(firstVisibleBlock, offset + (this.isMobile ? this.blockWidth : 0));
|
||||||
} else {
|
} else {
|
||||||
this.updatePages();
|
this.updatePages();
|
||||||
}
|
}
|
||||||
@ -178,8 +178,10 @@ export class StartComponent implements OnInit, OnDestroy {
|
|||||||
setTimeout(() => { this.scrollToBlock(height, blockOffset); }, 50);
|
setTimeout(() => { this.scrollToBlock(height, blockOffset); }, 50);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const targetHeight = this.isMobile ? height - 1 : height;
|
if (this.isMobile) {
|
||||||
const viewingPageIndex = this.getPageIndexOf(targetHeight);
|
blockOffset -= this.blockWidth;
|
||||||
|
}
|
||||||
|
const viewingPageIndex = this.getPageIndexOf(height);
|
||||||
const pages = [];
|
const pages = [];
|
||||||
this.pageIndex = Math.max(viewingPageIndex - 1, 0);
|
this.pageIndex = Math.max(viewingPageIndex - 1, 0);
|
||||||
let viewingPage = this.getPageAt(viewingPageIndex);
|
let viewingPage = this.getPageAt(viewingPageIndex);
|
||||||
@ -189,7 +191,7 @@ export class StartComponent implements OnInit, OnDestroy {
|
|||||||
viewingPage = this.getPageAt(viewingPageIndex);
|
viewingPage = this.getPageAt(viewingPageIndex);
|
||||||
}
|
}
|
||||||
const left = viewingPage.offset - this.getConvertedScrollOffset();
|
const left = viewingPage.offset - this.getConvertedScrollOffset();
|
||||||
const blockIndex = viewingPage.height - targetHeight;
|
const blockIndex = viewingPage.height - height;
|
||||||
const targetOffset = (this.blockWidth * blockIndex) + left;
|
const targetOffset = (this.blockWidth * blockIndex) + left;
|
||||||
let deltaOffset = targetOffset - blockOffset;
|
let deltaOffset = targetOffset - blockOffset;
|
||||||
|
|
||||||
|
@ -327,9 +327,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.fetchRbfHistory$.next(this.tx.txid);
|
this.fetchRbfHistory$.next(this.tx.txid);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.priceService.getPrices().pipe(
|
this.priceService.getBlockPrice$(tx.status.block_time, true).pipe(
|
||||||
tap(() => {
|
tap((price) => {
|
||||||
this.blockConversion = this.priceService.getPriceForTimestamp(tx.status.block_time);
|
this.blockConversion = price;
|
||||||
})
|
})
|
||||||
).subscribe();
|
).subscribe();
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.inter
|
|||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { AssetsService } from '../../services/assets.service';
|
import { AssetsService } from '../../services/assets.service';
|
||||||
import { filter, map, tap, switchMap } from 'rxjs/operators';
|
import { filter, map, tap, switchMap, shareReplay } from 'rxjs/operators';
|
||||||
import { BlockExtended } from '../../interfaces/node-api.interface';
|
import { BlockExtended } from '../../interfaces/node-api.interface';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { PriceService } from 'src/app/services/price.service';
|
import { PriceService } from 'src/app/services/price.service';
|
||||||
@ -150,10 +150,8 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
tx['addressValue'] = addressIn - addressOut;
|
tx['addressValue'] = addressIn - addressOut;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.priceService.getPrices().pipe(
|
this.priceService.getBlockPrice$(tx.status.block_time).pipe(
|
||||||
tap(() => {
|
tap((price) => tx['price'] = price)
|
||||||
tx['price'] = this.priceService.getPriceForTimestamp(tx.status.block_time);
|
|
||||||
})
|
|
||||||
).subscribe();
|
).subscribe();
|
||||||
});
|
});
|
||||||
const txIds = this.transactions.filter((tx) => !tx._outspends).map((tx) => tx.txid);
|
const txIds = this.transactions.filter((tx) => !tx._outspends).map((tx) => tx.txid);
|
||||||
|
@ -56,7 +56,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<p *ngIf="line.value == null && line.confidential" i18n="shared.confidential">Confidential</p>
|
<p *ngIf="line.value == null && line.confidential" i18n="shared.confidential">Confidential</p>
|
||||||
<p *ngIf="line.value != null"><app-amount [satoshis]="line.value"></app-amount></p>
|
<p *ngIf="line.value != null"><app-amount [blockConversion]="blockConversion" [satoshis]="line.value"></app-amount></p>
|
||||||
<p *ngIf="line.type !== 'fee' && line.address" class="address">
|
<p *ngIf="line.type !== 'fee' && line.address" class="address">
|
||||||
<app-truncate [text]="line.address"></app-truncate>
|
<app-truncate [text]="line.address"></app-truncate>
|
||||||
</p>
|
</p>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Component, ElementRef, ViewChild, Input, OnChanges, ChangeDetectionStrategy } from '@angular/core';
|
import { Component, ElementRef, ViewChild, Input, OnChanges, OnInit } from '@angular/core';
|
||||||
import { TransactionStripped } from '../../interfaces/websocket.interface';
|
import { tap } from 'rxjs';
|
||||||
|
import { Price, PriceService } from 'src/app/services/price.service';
|
||||||
|
|
||||||
interface Xput {
|
interface Xput {
|
||||||
type: 'input' | 'output' | 'fee';
|
type: 'input' | 'output' | 'fee';
|
||||||
@ -14,6 +15,7 @@ interface Xput {
|
|||||||
pegin?: boolean;
|
pegin?: boolean;
|
||||||
pegout?: string;
|
pegout?: string;
|
||||||
confidential?: boolean;
|
confidential?: boolean;
|
||||||
|
timestamp?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -27,12 +29,21 @@ export class TxBowtieGraphTooltipComponent implements OnChanges {
|
|||||||
@Input() isConnector: boolean = false;
|
@Input() isConnector: boolean = false;
|
||||||
|
|
||||||
tooltipPosition = { x: 0, y: 0 };
|
tooltipPosition = { x: 0, y: 0 };
|
||||||
|
blockConversion: Price;
|
||||||
|
|
||||||
@ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>;
|
@ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>;
|
||||||
|
|
||||||
constructor() {}
|
constructor(private priceService: PriceService) {}
|
||||||
|
|
||||||
ngOnChanges(changes): void {
|
ngOnChanges(changes): void {
|
||||||
|
if (changes.line?.currentValue) {
|
||||||
|
this.priceService.getBlockPrice$(changes.line?.currentValue.timestamp, true).pipe(
|
||||||
|
tap((price) => {
|
||||||
|
this.blockConversion = price;
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
if (changes.cursorPosition && changes.cursorPosition.currentValue) {
|
if (changes.cursorPosition && changes.cursorPosition.currentValue) {
|
||||||
let x = Math.max(10, changes.cursorPosition.currentValue.x - 50);
|
let x = Math.max(10, changes.cursorPosition.currentValue.x - 50);
|
||||||
let y = changes.cursorPosition.currentValue.y + 20;
|
let y = changes.cursorPosition.currentValue.y + 20;
|
||||||
|
@ -29,6 +29,7 @@ interface Xput {
|
|||||||
pegin?: boolean;
|
pegin?: boolean;
|
||||||
pegout?: string;
|
pegout?: string;
|
||||||
confidential?: boolean;
|
confidential?: boolean;
|
||||||
|
timestamp?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -152,6 +153,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
|||||||
index: i,
|
index: i,
|
||||||
pegout: v?.pegout?.scriptpubkey_address,
|
pegout: v?.pegout?.scriptpubkey_address,
|
||||||
confidential: (this.isLiquid && v?.value === undefined),
|
confidential: (this.isLiquid && v?.value === undefined),
|
||||||
|
timestamp: this.tx.status.block_time
|
||||||
} as Xput;
|
} as Xput;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -171,6 +173,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
|||||||
coinbase: v?.is_coinbase,
|
coinbase: v?.is_coinbase,
|
||||||
pegin: v?.is_pegin,
|
pegin: v?.is_pegin,
|
||||||
confidential: (this.isLiquid && v?.prevout?.value === undefined),
|
confidential: (this.isLiquid && v?.prevout?.value === undefined),
|
||||||
|
timestamp: this.tx.status.block_time
|
||||||
} as Xput;
|
} as Xput;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<span class="green-color" *ngIf="blockConversion; else noblockconversion">
|
<span class="green-color" *ngIf="blockConversion; else noblockconversion">
|
||||||
{{
|
{{
|
||||||
(
|
(
|
||||||
(blockConversion.price[currency] > 0 ? blockConversion.price[currency] : null) ??
|
(blockConversion.price[currency] >= 0 ? blockConversion.price[currency] : null) ??
|
||||||
(blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency]) ?? 0
|
(blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency]) ?? 0
|
||||||
) * value / 100000000 | fiatCurrency : digitsInfo : currency
|
) * value / 100000000 | fiatCurrency : digitsInfo : currency
|
||||||
}}
|
}}
|
||||||
|
@ -130,7 +130,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
|||||||
},
|
},
|
||||||
text: $localize`:@@b420668a91f8ebaf6e6409c4ba87f1d45961d2bd:Lightning Nodes Per Network`,
|
text: $localize`:@@b420668a91f8ebaf6e6409c4ba87f1d45961d2bd:Lightning Nodes Per Network`,
|
||||||
left: 'center',
|
left: 'center',
|
||||||
top: 11,
|
top: 0,
|
||||||
zlevel: 10,
|
zlevel: 10,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -227,8 +227,8 @@ export class NodesNetworksChartComponent implements OnInit {
|
|||||||
title: title,
|
title: title,
|
||||||
animation: false,
|
animation: false,
|
||||||
grid: {
|
grid: {
|
||||||
height: this.widget ? 100 : undefined,
|
height: this.widget ? 90 : undefined,
|
||||||
top: this.widget ? 10 : 40,
|
top: this.widget ? 20 : 40,
|
||||||
bottom: this.widget ? 0 : 70,
|
bottom: this.widget ? 0 : 70,
|
||||||
right: (isMobile() && this.widget) ? 35 : this.right,
|
right: (isMobile() && this.widget) ? 35 : this.right,
|
||||||
left: (isMobile() && this.widget) ? 40 :this.left,
|
left: (isMobile() && this.widget) ? 40 :this.left,
|
||||||
|
@ -121,7 +121,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
|||||||
},
|
},
|
||||||
text: $localize`:@@ea8db27e6db64f8b940711948c001a1100e5fe9f:Lightning Network Capacity`,
|
text: $localize`:@@ea8db27e6db64f8b940711948c001a1100e5fe9f:Lightning Network Capacity`,
|
||||||
left: 'center',
|
left: 'center',
|
||||||
top: 11,
|
top: 0,
|
||||||
zlevel: 10,
|
zlevel: 10,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -137,8 +137,8 @@ export class LightningStatisticsChartComponent implements OnInit {
|
|||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
grid: {
|
grid: {
|
||||||
height: this.widget ? 100 : undefined,
|
height: this.widget ? 90 : undefined,
|
||||||
top: this.widget ? 10 : 40,
|
top: this.widget ? 20 : 40,
|
||||||
bottom: this.widget ? 0 : 70,
|
bottom: this.widget ? 0 : 70,
|
||||||
right: (isMobile() && this.widget) ? 35 : this.right,
|
right: (isMobile() && this.widget) ? 35 : this.right,
|
||||||
left: (isMobile() && this.widget) ? 40 :this.left,
|
left: (isMobile() && this.widget) ? 40 :this.left,
|
||||||
|
@ -305,7 +305,10 @@ export class ApiService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getHistoricalPrice$(): Observable<Conversion> {
|
getHistoricalPrice$(timestamp: number | undefined): Observable<Conversion> {
|
||||||
return this.httpClient.get<Conversion>( this.apiBaseUrl + this.apiBasePath + '/api/v1/historical-price');
|
return this.httpClient.get<Conversion>(
|
||||||
|
this.apiBaseUrl + this.apiBasePath + '/api/v1/historical-price' +
|
||||||
|
(timestamp ? `?timestamp=${timestamp}` : '')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { map, Observable, of, shareReplay } from 'rxjs';
|
import { map, Observable, of, share, shareReplay, tap } from 'rxjs';
|
||||||
import { ApiService } from './api.service';
|
import { ApiService } from './api.service';
|
||||||
|
import { StateService } from './state.service';
|
||||||
|
|
||||||
// nodejs backend interfaces
|
// nodejs backend interfaces
|
||||||
export interface ApiPrice {
|
export interface ApiPrice {
|
||||||
@ -40,13 +41,20 @@ export interface ConversionDict {
|
|||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class PriceService {
|
export class PriceService {
|
||||||
|
priceObservable$: Observable<Conversion>;
|
||||||
|
singlePriceObservable$: Observable<Conversion>;
|
||||||
|
|
||||||
|
lastQueriedTimestamp: number;
|
||||||
|
lastPriceHistoryUpdate: number;
|
||||||
|
|
||||||
historicalPrice: ConversionDict = {
|
historicalPrice: ConversionDict = {
|
||||||
prices: null,
|
prices: null,
|
||||||
exchangeRates: null,
|
exchangeRates: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private apiService: ApiService
|
private apiService: ApiService,
|
||||||
|
private stateService: StateService
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,50 +69,73 @@ export class PriceService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getBlockPrice$(blockTimestamp: number, singlePrice = false): Observable<Price | undefined> {
|
||||||
* Fetch prices from the nodejs backend only once
|
if (this.stateService.env.BASE_MODULE !== 'mempool') {
|
||||||
*/
|
return of(undefined);
|
||||||
getPrices(): Observable<void> {
|
|
||||||
if (this.historicalPrice.prices) {
|
|
||||||
return of(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.apiService.getHistoricalPrice$().pipe(
|
const now = new Date().getTime() / 1000;
|
||||||
map((conversion: Conversion) => {
|
|
||||||
if (!this.historicalPrice.prices) {
|
/**
|
||||||
this.historicalPrice.prices = Object();
|
* Query nearest price for a specific blockTimestamp. The observable is invalidated if we
|
||||||
|
* query a different timestamp than the last one
|
||||||
|
*/
|
||||||
|
if (singlePrice) {
|
||||||
|
if (!this.singlePriceObservable$ || (this.singlePriceObservable$ && blockTimestamp !== this.lastQueriedTimestamp)) {
|
||||||
|
this.singlePriceObservable$ = this.apiService.getHistoricalPrice$(blockTimestamp).pipe(shareReplay());
|
||||||
|
this.lastQueriedTimestamp = blockTimestamp;
|
||||||
}
|
}
|
||||||
for (const price of conversion.prices) {
|
|
||||||
this.historicalPrice.prices[price.time] = {
|
return this.singlePriceObservable$.pipe(
|
||||||
USD: price.USD, EUR: price.EUR, GBP: price.GBP, CAD: price.CAD,
|
map((conversion) => {
|
||||||
CHF: price.CHF, AUD: price.AUD, JPY: price.JPY
|
if (conversion.prices.length <= 0) {
|
||||||
|
return this.getEmptyPrice();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
price: {
|
||||||
|
USD: conversion.prices[0].USD, EUR: conversion.prices[0].EUR, GBP: conversion.prices[0].GBP, CAD: conversion.prices[0].CAD,
|
||||||
|
CHF: conversion.prices[0].CHF, AUD: conversion.prices[0].AUD, JPY: conversion.prices[0].JPY
|
||||||
|
},
|
||||||
|
exchangeRates: conversion.exchangeRates,
|
||||||
};
|
};
|
||||||
}
|
})
|
||||||
this.historicalPrice.exchangeRates = conversion.exchangeRates;
|
|
||||||
return;
|
|
||||||
}),
|
|
||||||
shareReplay(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note: The first block with a price we have is block 68952 (using MtGox price history)
|
* Query all price history only once. The observable is invalidated after 1 hour
|
||||||
*
|
|
||||||
* @param blockTimestamp
|
|
||||||
*/
|
*/
|
||||||
getPriceForTimestamp(blockTimestamp: number): Price | null {
|
else {
|
||||||
|
if (!this.priceObservable$ || (this.priceObservable$ && (now - this.lastPriceHistoryUpdate > 3600))) {
|
||||||
|
this.priceObservable$ = this.apiService.getHistoricalPrice$(undefined).pipe(shareReplay());
|
||||||
|
this.lastPriceHistoryUpdate = new Date().getTime() / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.priceObservable$.pipe(
|
||||||
|
map((conversion) => {
|
||||||
if (!blockTimestamp) {
|
if (!blockTimestamp) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const priceTimestamps = Object.keys(this.historicalPrice.prices);
|
const historicalPrice = {
|
||||||
|
prices: {},
|
||||||
|
exchangeRates: conversion.exchangeRates,
|
||||||
|
};
|
||||||
|
for (const price of conversion.prices) {
|
||||||
|
historicalPrice.prices[price.time] = {
|
||||||
|
USD: price.USD, EUR: price.EUR, GBP: price.GBP, CAD: price.CAD,
|
||||||
|
CHF: price.CHF, AUD: price.AUD, JPY: price.JPY
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const priceTimestamps = Object.keys(historicalPrice.prices);
|
||||||
priceTimestamps.push(Number.MAX_SAFE_INTEGER.toString());
|
priceTimestamps.push(Number.MAX_SAFE_INTEGER.toString());
|
||||||
priceTimestamps.sort().reverse();
|
priceTimestamps.sort().reverse();
|
||||||
|
|
||||||
// Small trick here. Because latest blocks have higher timestamps than our
|
// Small trick here. Because latest blocks have higher timestamps than our
|
||||||
// latest price timestamp (we only insert once every hour), we have no price for them.
|
// latest price timestamp (we only insert once every hour), we have no price for them.
|
||||||
// Therefore we want to fallback to the websocket price by returning an undefined `price` field.
|
// Therefore we want to fallback to the websocket price by returning an undefined `price` field.
|
||||||
// Since this.historicalPrice.prices[Number.MAX_SAFE_INTEGER] does not exists
|
// Since historicalPrice.prices[Number.MAX_SAFE_INTEGER] does not exists
|
||||||
// it will return `undefined` and automatically use the websocket price.
|
// it will return `undefined` and automatically use the websocket price.
|
||||||
// This way we can differenciate blocks without prices like the genesis block
|
// This way we can differenciate blocks without prices like the genesis block
|
||||||
// vs ones without a price (yet) like the latest blocks
|
// vs ones without a price (yet) like the latest blocks
|
||||||
@ -113,13 +144,15 @@ export class PriceService {
|
|||||||
const priceTimestamp = parseInt(t, 10);
|
const priceTimestamp = parseInt(t, 10);
|
||||||
if (blockTimestamp > priceTimestamp) {
|
if (blockTimestamp > priceTimestamp) {
|
||||||
return {
|
return {
|
||||||
price: this.historicalPrice.prices[priceTimestamp],
|
price: historicalPrice.prices[priceTimestamp],
|
||||||
exchangeRates: this.historicalPrice.exchangeRates,
|
exchangeRates: historicalPrice.exchangeRates,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getEmptyPrice();
|
return this.getEmptyPrice();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user