Optimize price API response size reduce the number of query to that API
This commit is contained in:
parent
f6c7839524
commit
5749820999
@ -41,7 +41,13 @@ class MiningRoutes {
|
|||||||
res.header('Pragma', 'public');
|
res.header('Pragma', 'public');
|
||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||||
res.status(200).send(await PricesRepository.$getHistoricalPrice());
|
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);
|
||||||
}
|
}
|
||||||
|
@ -104,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`);
|
||||||
}
|
}
|
||||||
@ -127,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -327,7 +327,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.fetchRbfHistory$.next(this.tx.txid);
|
this.fetchRbfHistory$.next(this.tx.txid);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.priceService.getBlockPrice$(tx.status.block_time).pipe(
|
this.priceService.getBlockPrice$(tx.status.block_time, true).pipe(
|
||||||
tap((price) => {
|
tap((price) => {
|
||||||
this.blockConversion = price;
|
this.blockConversion = price;
|
||||||
})
|
})
|
||||||
|
@ -37,7 +37,7 @@ export class TxBowtieGraphTooltipComponent implements OnChanges {
|
|||||||
|
|
||||||
ngOnChanges(changes): void {
|
ngOnChanges(changes): void {
|
||||||
if (changes.line?.currentValue) {
|
if (changes.line?.currentValue) {
|
||||||
this.priceService.getBlockPrice$(changes.line?.currentValue.timestamp).pipe(
|
this.priceService.getBlockPrice$(changes.line?.currentValue.timestamp, true).pipe(
|
||||||
tap((price) => {
|
tap((price) => {
|
||||||
this.blockConversion = price;
|
this.blockConversion = price;
|
||||||
})
|
})
|
||||||
|
@ -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}` : '')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,10 @@ export interface ConversionDict {
|
|||||||
})
|
})
|
||||||
export class PriceService {
|
export class PriceService {
|
||||||
priceObservable$: Observable<Conversion>;
|
priceObservable$: Observable<Conversion>;
|
||||||
|
singlePriceObservable$: Observable<Conversion>;
|
||||||
|
|
||||||
|
lastQueriedTimestamp: number;
|
||||||
|
lastPriceHistoryUpdate: number;
|
||||||
|
|
||||||
historicalPrice: ConversionDict = {
|
historicalPrice: ConversionDict = {
|
||||||
prices: null,
|
prices: null,
|
||||||
@ -63,53 +67,83 @@ export class PriceService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getBlockPrice$(blockTimestamp: number): Observable<Price | undefined> {
|
getBlockPrice$(blockTimestamp: number, singlePrice = false): Observable<Price | undefined> {
|
||||||
if (!this.priceObservable$) {
|
const now = new Date().getTime() / 1000;
|
||||||
this.priceObservable$ = this.apiService.getHistoricalPrice$().pipe(shareReplay());
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.singlePriceObservable$.pipe(
|
||||||
|
map((conversion) => {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.priceObservable$.pipe(
|
/**
|
||||||
map((conversion) => {
|
* Query all price history only once. The observable is invalidated after 1 hour
|
||||||
if (!blockTimestamp) {
|
*/
|
||||||
return undefined;
|
else {
|
||||||
}
|
if (!this.priceObservable$ || (this.priceObservable$ && (now - this.lastPriceHistoryUpdate > 3600))) {
|
||||||
|
this.priceObservable$ = this.apiService.getHistoricalPrice$(undefined).pipe(shareReplay());
|
||||||
|
this.lastPriceHistoryUpdate = new Date().getTime() / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
const historicalPrice = {
|
return this.priceObservable$.pipe(
|
||||||
prices: {},
|
map((conversion) => {
|
||||||
exchangeRates: conversion.exchangeRates,
|
if (!blockTimestamp) {
|
||||||
};
|
return undefined;
|
||||||
for (const price of conversion.prices) {
|
}
|
||||||
historicalPrice.prices[price.time] = {
|
|
||||||
USD: price.USD, EUR: price.EUR, GBP: price.GBP, CAD: price.CAD,
|
const historicalPrice = {
|
||||||
CHF: price.CHF, AUD: price.AUD, JPY: price.JPY
|
prices: {},
|
||||||
|
exchangeRates: conversion.exchangeRates,
|
||||||
};
|
};
|
||||||
}
|
for (const price of conversion.prices) {
|
||||||
|
historicalPrice.prices[price.time] = {
|
||||||
const priceTimestamps = Object.keys(historicalPrice.prices);
|
USD: price.USD, EUR: price.EUR, GBP: price.GBP, CAD: price.CAD,
|
||||||
priceTimestamps.push(Number.MAX_SAFE_INTEGER.toString());
|
CHF: price.CHF, AUD: price.AUD, JPY: price.JPY
|
||||||
priceTimestamps.sort().reverse();
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
// Therefore we want to fallback to the websocket price by returning an undefined `price` field.
|
|
||||||
// Since historicalPrice.prices[Number.MAX_SAFE_INTEGER] does not exists
|
|
||||||
// it will return `undefined` and automatically use the websocket price.
|
|
||||||
// This way we can differenciate blocks without prices like the genesis block
|
|
||||||
// vs ones without a price (yet) like the latest blocks
|
|
||||||
|
|
||||||
for (const t of priceTimestamps) {
|
|
||||||
const priceTimestamp = parseInt(t, 10);
|
|
||||||
if (blockTimestamp > priceTimestamp) {
|
|
||||||
return {
|
|
||||||
price: historicalPrice.prices[priceTimestamp],
|
|
||||||
exchangeRates: historicalPrice.exchangeRates,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
const priceTimestamps = Object.keys(historicalPrice.prices);
|
||||||
return this.getEmptyPrice();
|
priceTimestamps.push(Number.MAX_SAFE_INTEGER.toString());
|
||||||
})
|
priceTimestamps.sort().reverse();
|
||||||
);
|
|
||||||
|
// 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.
|
||||||
|
// Therefore we want to fallback to the websocket price by returning an undefined `price` field.
|
||||||
|
// Since historicalPrice.prices[Number.MAX_SAFE_INTEGER] does not exists
|
||||||
|
// it will return `undefined` and automatically use the websocket price.
|
||||||
|
// This way we can differenciate blocks without prices like the genesis block
|
||||||
|
// vs ones without a price (yet) like the latest blocks
|
||||||
|
|
||||||
|
for (const t of priceTimestamps) {
|
||||||
|
const priceTimestamp = parseInt(t, 10);
|
||||||
|
if (blockTimestamp > priceTimestamp) {
|
||||||
|
return {
|
||||||
|
price: historicalPrice.prices[priceTimestamp],
|
||||||
|
exchangeRates: historicalPrice.exchangeRates,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getEmptyPrice();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user