Optimize price API response size reduce the number of query to that API

This commit is contained in:
nymkappa 2023-02-23 13:13:20 +09:00
parent f6c7839524
commit 5749820999
No known key found for this signature in database
GPG Key ID: E155910B16E8BD04
6 changed files with 132 additions and 50 deletions

View File

@ -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);
} }

View File

@ -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;
} }
} }

View File

@ -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;
}) })

View File

@ -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;
}) })

View File

@ -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}` : '')
);
} }
} }

View File

@ -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();
})
);
}
} }
} }