2022-06-23 15:42:42 +02:00
|
|
|
import DB from '../database';
|
|
|
|
import logger from '../logger';
|
2023-02-15 16:05:14 +09:00
|
|
|
import priceUpdater from '../tasks/price-updater';
|
2022-06-23 15:42:42 +02:00
|
|
|
|
2023-02-21 12:36:43 +09:00
|
|
|
export interface ApiPrice {
|
|
|
|
time?: number,
|
|
|
|
USD: number,
|
|
|
|
EUR: number,
|
|
|
|
GBP: number,
|
|
|
|
CAD: number,
|
|
|
|
CHF: number,
|
|
|
|
AUD: number,
|
|
|
|
JPY: number,
|
|
|
|
}
|
2023-03-04 10:51:13 +09:00
|
|
|
const ApiPriceFields = `
|
|
|
|
UNIX_TIMESTAMP(time) as time,
|
|
|
|
USD,
|
|
|
|
EUR,
|
|
|
|
GBP,
|
|
|
|
CAD,
|
|
|
|
CHF,
|
|
|
|
AUD,
|
|
|
|
JPY
|
|
|
|
`;
|
2023-02-21 12:36:43 +09:00
|
|
|
|
|
|
|
export interface ExchangeRates {
|
|
|
|
USDEUR: number,
|
|
|
|
USDGBP: number,
|
|
|
|
USDCAD: number,
|
|
|
|
USDCHF: number,
|
|
|
|
USDAUD: number,
|
|
|
|
USDJPY: number,
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface Conversion {
|
|
|
|
prices: ApiPrice[],
|
|
|
|
exchangeRates: ExchangeRates;
|
|
|
|
}
|
|
|
|
|
2023-02-23 09:50:34 +09:00
|
|
|
export const MAX_PRICES = {
|
|
|
|
USD: 100000000,
|
|
|
|
EUR: 100000000,
|
|
|
|
GBP: 100000000,
|
|
|
|
CAD: 100000000,
|
|
|
|
CHF: 100000000,
|
|
|
|
AUD: 100000000,
|
|
|
|
JPY: 10000000000,
|
|
|
|
};
|
|
|
|
|
2022-06-23 15:42:42 +02:00
|
|
|
class PricesRepository {
|
2023-03-04 10:51:13 +09:00
|
|
|
public async $savePrices(time: number, prices: ApiPrice): Promise<void> {
|
2023-03-01 19:11:03 +09:00
|
|
|
if (prices.USD === -1) {
|
2023-02-15 16:05:14 +09:00
|
|
|
// Some historical price entries have no USD prices, so we just ignore them to avoid future UX issues
|
|
|
|
// As of today there are only 4 (on 2013-09-05, 2013-0909, 2013-09-12 and 2013-09-26) so that's fine
|
2022-07-09 16:13:01 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-02-23 09:50:34 +09:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-23 15:42:42 +02:00
|
|
|
try {
|
2022-06-26 13:49:39 +02:00
|
|
|
await DB.query(`
|
|
|
|
INSERT INTO prices(time, USD, EUR, GBP, CAD, CHF, AUD, JPY)
|
|
|
|
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ? )`,
|
|
|
|
[time, prices.USD, prices.EUR, prices.GBP, prices.CAD, prices.CHF, prices.AUD, prices.JPY]
|
|
|
|
);
|
2023-03-04 10:51:13 +09:00
|
|
|
} catch (e) {
|
2022-06-23 15:42:42 +02:00
|
|
|
logger.err(`Cannot save exchange rate into db. Reason: ` + (e instanceof Error ? e.message : e));
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public async $getOldestPriceTime(): Promise<number> {
|
2023-03-04 10:51:13 +09:00
|
|
|
const [oldestRow] = await DB.query(`
|
|
|
|
SELECT UNIX_TIMESTAMP(time) AS time
|
|
|
|
FROM prices
|
|
|
|
ORDER BY time
|
|
|
|
LIMIT 1
|
|
|
|
`);
|
2022-06-23 15:42:42 +02:00
|
|
|
return oldestRow[0] ? oldestRow[0].time : 0;
|
|
|
|
}
|
|
|
|
|
2022-07-16 09:22:45 +02:00
|
|
|
public async $getLatestPriceId(): Promise<number | null> {
|
2023-03-04 10:51:13 +09:00
|
|
|
const [oldestRow] = await DB.query(`
|
|
|
|
SELECT id
|
|
|
|
FROM prices
|
|
|
|
ORDER BY time DESC
|
|
|
|
LIMIT 1`
|
|
|
|
);
|
2022-07-16 09:22:45 +02:00
|
|
|
return oldestRow[0] ? oldestRow[0].id : null;
|
|
|
|
}
|
|
|
|
|
2022-06-23 15:42:42 +02:00
|
|
|
public async $getLatestPriceTime(): Promise<number> {
|
2023-03-04 10:51:13 +09:00
|
|
|
const [oldestRow] = await DB.query(`
|
|
|
|
SELECT UNIX_TIMESTAMP(time) AS time
|
|
|
|
FROM prices
|
|
|
|
ORDER BY time DESC
|
|
|
|
LIMIT 1`
|
|
|
|
);
|
2022-06-23 15:42:42 +02:00
|
|
|
return oldestRow[0] ? oldestRow[0].time : 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
public async $getPricesTimes(): Promise<number[]> {
|
2023-03-04 10:51:13 +09:00
|
|
|
const [times] = await DB.query(`
|
|
|
|
SELECT UNIX_TIMESTAMP(time) AS time
|
|
|
|
FROM prices
|
|
|
|
WHERE USD != -1
|
|
|
|
ORDER BY time
|
|
|
|
`);
|
|
|
|
if (!Array.isArray(times)) {
|
|
|
|
return [];
|
|
|
|
}
|
2022-06-23 15:42:42 +02:00
|
|
|
return times.map(time => time.time);
|
|
|
|
}
|
2022-07-09 16:53:29 +02:00
|
|
|
|
2023-03-04 10:51:13 +09:00
|
|
|
public async $getPricesTimesAndId(): Promise<{time: number, id: number, USD: number}[]> {
|
|
|
|
const [times] = await DB.query(`
|
|
|
|
SELECT
|
|
|
|
UNIX_TIMESTAMP(time) AS time,
|
|
|
|
id,
|
|
|
|
USD
|
|
|
|
FROM prices
|
|
|
|
ORDER BY time
|
|
|
|
`);
|
|
|
|
return times as {time: number, id: number, USD: number}[];
|
2022-07-09 16:53:29 +02:00
|
|
|
}
|
2023-02-15 16:05:14 +09:00
|
|
|
|
2023-03-04 10:51:13 +09:00
|
|
|
public async $getLatestConversionRates(): Promise<ApiPrice> {
|
|
|
|
const [rates] = await DB.query(`
|
|
|
|
SELECT ${ApiPriceFields}
|
2023-02-15 16:05:14 +09:00
|
|
|
FROM prices
|
|
|
|
ORDER BY time DESC
|
|
|
|
LIMIT 1`
|
|
|
|
);
|
2023-03-04 10:51:13 +09:00
|
|
|
|
|
|
|
if (!Array.isArray(rates) || rates.length === 0) {
|
2023-02-15 16:05:14 +09:00
|
|
|
return priceUpdater.getEmptyPricesObj();
|
|
|
|
}
|
2023-03-04 10:51:13 +09:00
|
|
|
return rates[0] as ApiPrice;
|
2023-02-15 16:05:14 +09:00
|
|
|
}
|
2023-02-21 12:36:43 +09:00
|
|
|
|
2023-02-23 13:13:20 +09:00
|
|
|
public async $getNearestHistoricalPrice(timestamp: number | undefined): Promise<Conversion | null> {
|
2023-02-21 12:36:43 +09:00
|
|
|
try {
|
2023-03-04 10:51:13 +09:00
|
|
|
const [rates] = await DB.query(`
|
|
|
|
SELECT ${ApiPriceFields}
|
2023-02-23 13:13:20 +09:00
|
|
|
FROM prices
|
|
|
|
WHERE UNIX_TIMESTAMP(time) < ?
|
|
|
|
ORDER BY time DESC
|
|
|
|
LIMIT 1`,
|
|
|
|
[timestamp]
|
|
|
|
);
|
2023-03-04 10:51:13 +09:00
|
|
|
if (!Array.isArray(rates)) {
|
2023-02-23 13:13:20 +09:00
|
|
|
throw Error(`Cannot get single historical price from the database`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Compute fiat exchange rates
|
2023-03-04 10:51:13 +09:00
|
|
|
let latestPrice = rates[0] as ApiPrice;
|
2023-03-27 20:02:33 +09:00
|
|
|
if (!latestPrice || latestPrice.USD === -1) {
|
2023-03-04 10:51:13 +09:00
|
|
|
latestPrice = priceUpdater.getEmptyPricesObj();
|
|
|
|
}
|
|
|
|
|
|
|
|
const computeFx = (usd: number, other: number): number =>
|
|
|
|
Math.round(Math.max(other, 0) / Math.max(usd, 1) * 100) / 100;
|
|
|
|
|
2023-02-23 13:13:20 +09:00
|
|
|
const exchangeRates: ExchangeRates = {
|
2023-03-04 10:51:13 +09:00
|
|
|
USDEUR: computeFx(latestPrice.USD, latestPrice.EUR),
|
|
|
|
USDGBP: computeFx(latestPrice.USD, latestPrice.GBP),
|
|
|
|
USDCAD: computeFx(latestPrice.USD, latestPrice.CAD),
|
|
|
|
USDCHF: computeFx(latestPrice.USD, latestPrice.CHF),
|
|
|
|
USDAUD: computeFx(latestPrice.USD, latestPrice.AUD),
|
|
|
|
USDJPY: computeFx(latestPrice.USD, latestPrice.JPY),
|
2023-02-23 13:13:20 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
2023-03-04 10:51:13 +09:00
|
|
|
prices: rates as ApiPrice[],
|
2023-02-23 13:13:20 +09:00
|
|
|
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 {
|
2023-03-04 10:51:13 +09:00
|
|
|
const [rates] = await DB.query(`
|
|
|
|
SELECT ${ApiPriceFields}
|
2023-02-23 13:13:20 +09:00
|
|
|
FROM prices
|
|
|
|
ORDER BY time DESC
|
|
|
|
`);
|
2023-03-04 10:51:13 +09:00
|
|
|
if (!Array.isArray(rates)) {
|
2023-02-21 12:36:43 +09:00
|
|
|
throw Error(`Cannot get average historical price from the database`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Compute fiat exchange rates
|
2023-03-04 10:51:13 +09:00
|
|
|
let latestPrice = rates[0] as ApiPrice;
|
|
|
|
if (latestPrice.USD === -1) {
|
|
|
|
latestPrice = priceUpdater.getEmptyPricesObj();
|
|
|
|
}
|
|
|
|
|
|
|
|
const computeFx = (usd: number, other: number): number =>
|
|
|
|
Math.round(Math.max(other, 0) / Math.max(usd, 1) * 100) / 100;
|
|
|
|
|
2023-02-21 12:36:43 +09:00
|
|
|
const exchangeRates: ExchangeRates = {
|
2023-03-04 10:51:13 +09:00
|
|
|
USDEUR: computeFx(latestPrice.USD, latestPrice.EUR),
|
|
|
|
USDGBP: computeFx(latestPrice.USD, latestPrice.GBP),
|
|
|
|
USDCAD: computeFx(latestPrice.USD, latestPrice.CAD),
|
|
|
|
USDCHF: computeFx(latestPrice.USD, latestPrice.CHF),
|
|
|
|
USDAUD: computeFx(latestPrice.USD, latestPrice.AUD),
|
|
|
|
USDJPY: computeFx(latestPrice.USD, latestPrice.JPY),
|
2023-02-21 12:36:43 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
2023-03-04 10:51:13 +09:00
|
|
|
prices: rates as ApiPrice[],
|
2023-02-21 12:36:43 +09:00
|
|
|
exchangeRates: exchangeRates
|
|
|
|
};
|
|
|
|
} catch (e) {
|
2023-02-23 13:13:20 +09:00
|
|
|
logger.err(`Cannot fetch historical prices from the db. Reason ${e instanceof Error ? e.message : e}`);
|
2023-02-21 12:36:43 +09:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
2022-06-23 15:42:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export default new PricesRepository();
|
|
|
|
|