Merge pull request #4747 from mempool/natsoni/more-fiat-currencies
Use fx rates to add more currencies support and improve fiat prices
This commit is contained in:
commit
81c8c8dafb
@ -155,5 +155,9 @@
|
||||
"MEMPOOL_SERVICES": {
|
||||
"API": "https://mempool.space/api",
|
||||
"ACCELERATIONS": false
|
||||
},
|
||||
"FIAT_PRICE": {
|
||||
"ENABLED": true,
|
||||
"API_KEY": "your-api-key-from-freecurrencyapi.com"
|
||||
}
|
||||
}
|
||||
|
@ -147,5 +147,9 @@
|
||||
"ENABLED": false,
|
||||
"UNIX_SOCKET_PATH": "/tmp/redis.sock",
|
||||
"BATCH_QUERY_BASE_SIZE": 5000
|
||||
},
|
||||
"FIAT_PRICE": {
|
||||
"ENABLED": true,
|
||||
"API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
|
||||
}
|
||||
}
|
||||
|
@ -152,6 +152,11 @@ describe('Mempool Backend Config', () => {
|
||||
UNIX_SOCKET_PATH: '',
|
||||
BATCH_QUERY_BASE_SIZE: 5000,
|
||||
});
|
||||
|
||||
expect(config.FIAT_PRICE).toStrictEqual({
|
||||
ENABLED: true,
|
||||
API_KEY: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 74;
|
||||
private static currentVersion = 75;
|
||||
private queryTimeout = 3600_000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
@ -608,7 +608,7 @@ class DatabaseMigration {
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 72 && isBitcoin === true) {
|
||||
// reindex Goggles flags for mined block templates above height 833000
|
||||
// reindex Goggles flags for mined block templates above height 832000
|
||||
await this.$executeQuery('UPDATE blocks_summaries SET version = 0 WHERE height >= 832000;');
|
||||
await this.updateToSchemaVersion(72);
|
||||
}
|
||||
@ -624,6 +624,36 @@ class DatabaseMigration {
|
||||
await this.$executeQuery(`INSERT INTO state(name, number) VALUE ('last_acceleration_block', 0);`);
|
||||
await this.updateToSchemaVersion(74);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 75) {
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `BGN` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `BRL` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `CNY` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `CZK` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `DKK` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `HKD` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `HRK` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `HUF` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `IDR` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `ILS` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `INR` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `ISK` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `KRW` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `MXN` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `MYR` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `NOK` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `NZD` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `PHP` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `PLN` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `RON` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `RUB` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `SEK` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `SGD` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `THB` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `TRY` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"');
|
||||
await this.updateToSchemaVersion(75);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -51,13 +51,20 @@ class MiningRoutes {
|
||||
res.status(400).send('Prices are not available on testnets.');
|
||||
return;
|
||||
}
|
||||
if (req.query.timestamp) {
|
||||
res.status(200).send(await PricesRepository.$getNearestHistoricalPrice(
|
||||
parseInt(<string>req.query.timestamp ?? 0, 10)
|
||||
));
|
||||
const timestamp = parseInt(req.query.timestamp as string, 10) || 0;
|
||||
const currency = req.query.currency as string;
|
||||
|
||||
let response;
|
||||
if (timestamp && currency) {
|
||||
response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency);
|
||||
} else if (timestamp) {
|
||||
response = await PricesRepository.$getNearestHistoricalPrice(timestamp);
|
||||
} else if (currency) {
|
||||
response = await PricesRepository.$getHistoricalPrices(currency);
|
||||
} else {
|
||||
res.status(200).send(await PricesRepository.$getHistoricalPrices());
|
||||
response = await PricesRepository.$getHistoricalPrices();
|
||||
}
|
||||
res.status(200).send(response);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
|
@ -158,6 +158,10 @@ interface IConfig {
|
||||
UNIX_SOCKET_PATH: string;
|
||||
BATCH_QUERY_BASE_SIZE: number;
|
||||
},
|
||||
FIAT_PRICE: {
|
||||
ENABLED: boolean;
|
||||
API_KEY: string;
|
||||
},
|
||||
}
|
||||
|
||||
const defaults: IConfig = {
|
||||
@ -316,6 +320,10 @@ const defaults: IConfig = {
|
||||
'UNIX_SOCKET_PATH': '',
|
||||
'BATCH_QUERY_BASE_SIZE': 5000,
|
||||
},
|
||||
'FIAT_PRICE': {
|
||||
'ENABLED': true,
|
||||
'API_KEY': '',
|
||||
},
|
||||
};
|
||||
|
||||
class Config implements IConfig {
|
||||
@ -337,6 +345,7 @@ class Config implements IConfig {
|
||||
REPLICATION: IConfig['REPLICATION'];
|
||||
MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
|
||||
REDIS: IConfig['REDIS'];
|
||||
FIAT_PRICE: IConfig['FIAT_PRICE'];
|
||||
|
||||
constructor() {
|
||||
const configs = this.merge(configFromFile, defaults);
|
||||
@ -358,6 +367,7 @@ class Config implements IConfig {
|
||||
this.REPLICATION = configs.REPLICATION;
|
||||
this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
|
||||
this.REDIS = configs.REDIS;
|
||||
this.FIAT_PRICE = configs.FIAT_PRICE;
|
||||
}
|
||||
|
||||
merge = (...objects: object[]): IConfig => {
|
||||
|
@ -131,7 +131,7 @@ class Server {
|
||||
.use(express.text({ type: ['text/plain', 'application/base64'] }))
|
||||
;
|
||||
|
||||
if (config.DATABASE.ENABLED) {
|
||||
if (config.DATABASE.ENABLED && config.FIAT_PRICE.ENABLED) {
|
||||
await priceUpdater.$initializeLatestPriceWithDb();
|
||||
}
|
||||
|
||||
@ -168,7 +168,9 @@ class Server {
|
||||
setInterval(refreshIcons, 3600_000);
|
||||
}
|
||||
|
||||
priceUpdater.$run();
|
||||
if (config.FIAT_PRICE.ENABLED) {
|
||||
priceUpdater.$run();
|
||||
}
|
||||
await chainTips.updateOrphanedBlocks();
|
||||
|
||||
this.setUpHttpApiRoutes();
|
||||
@ -220,7 +222,9 @@ class Server {
|
||||
await memPool.$updateMempool(newMempool, newAccelerations, pollRate);
|
||||
}
|
||||
indexer.$run();
|
||||
priceUpdater.$run();
|
||||
if (config.FIAT_PRICE.ENABLED) {
|
||||
priceUpdater.$run();
|
||||
}
|
||||
|
||||
// rerun immediately if we skipped the mempool update, otherwise wait POLL_RATE_MS
|
||||
const elapsed = Date.now() - start;
|
||||
@ -284,7 +288,9 @@ class Server {
|
||||
memPool.setAsyncMempoolChangedCallback(websocketHandler.$handleMempoolChange.bind(websocketHandler));
|
||||
blocks.setNewAsyncBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
|
||||
}
|
||||
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
|
||||
if (config.FIAT_PRICE.ENABLED) {
|
||||
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
|
||||
}
|
||||
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
|
||||
}
|
||||
|
||||
|
@ -117,7 +117,7 @@ class Indexer {
|
||||
|
||||
switch (task) {
|
||||
case 'blocksPrices': {
|
||||
if (!['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
|
||||
if (!['testnet', 'signet'].includes(config.MEMPOOL.NETWORK) && config.FIAT_PRICE.ENABLED) {
|
||||
let lastestPriceId;
|
||||
try {
|
||||
lastestPriceId = await PricesRepository.$getLatestPriceId();
|
||||
@ -149,10 +149,12 @@ class Indexer {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await priceUpdater.$run();
|
||||
} catch (e) {
|
||||
logger.err(`Running priceUpdater failed. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
if (config.FIAT_PRICE.ENABLED) {
|
||||
try {
|
||||
await priceUpdater.$run();
|
||||
} catch (e) {
|
||||
logger.err(`Running priceUpdater failed. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
// Do not attempt to index anything unless Bitcoin Core is fully synced
|
||||
|
@ -1,5 +1,6 @@
|
||||
import DB from '../database';
|
||||
import logger from '../logger';
|
||||
import config from '../config';
|
||||
import priceUpdater from '../tasks/price-updater';
|
||||
|
||||
export interface ApiPrice {
|
||||
@ -11,17 +12,81 @@ export interface ApiPrice {
|
||||
CHF: number,
|
||||
AUD: number,
|
||||
JPY: number,
|
||||
BGN: number,
|
||||
BRL: number,
|
||||
CNY: number,
|
||||
CZK: number,
|
||||
DKK: number,
|
||||
HKD: number,
|
||||
HRK: number,
|
||||
HUF: number,
|
||||
IDR: number,
|
||||
ILS: number,
|
||||
INR: number,
|
||||
ISK: number,
|
||||
KRW: number,
|
||||
MXN: number,
|
||||
MYR: number,
|
||||
NOK: number,
|
||||
NZD: number,
|
||||
PHP: number,
|
||||
PLN: number,
|
||||
RON: number,
|
||||
RUB: number,
|
||||
SEK: number,
|
||||
SGD: number,
|
||||
THB: number,
|
||||
TRY: number,
|
||||
ZAR: number,
|
||||
}
|
||||
const ApiPriceFields = `
|
||||
UNIX_TIMESTAMP(time) as time,
|
||||
USD,
|
||||
EUR,
|
||||
GBP,
|
||||
CAD,
|
||||
CHF,
|
||||
AUD,
|
||||
JPY
|
||||
`;
|
||||
|
||||
const ApiPriceFields = config.FIAT_PRICE.API_KEY ?
|
||||
`
|
||||
UNIX_TIMESTAMP(time) as time,
|
||||
USD,
|
||||
EUR,
|
||||
GBP,
|
||||
CAD,
|
||||
CHF,
|
||||
AUD,
|
||||
JPY,
|
||||
BGN,
|
||||
BRL,
|
||||
CNY,
|
||||
CZK,
|
||||
DKK,
|
||||
HKD,
|
||||
HRK,
|
||||
HUF,
|
||||
IDR,
|
||||
ILS,
|
||||
INR,
|
||||
ISK,
|
||||
KRW,
|
||||
MXN,
|
||||
MYR,
|
||||
NOK,
|
||||
NZD,
|
||||
PHP,
|
||||
PLN,
|
||||
RON,
|
||||
RUB,
|
||||
SEK,
|
||||
SGD,
|
||||
THB,
|
||||
TRY,
|
||||
ZAR
|
||||
`:
|
||||
`
|
||||
UNIX_TIMESTAMP(time) as time,
|
||||
USD,
|
||||
EUR,
|
||||
GBP,
|
||||
CAD,
|
||||
CHF,
|
||||
AUD,
|
||||
JPY
|
||||
`;
|
||||
|
||||
export interface ExchangeRates {
|
||||
USDEUR: number,
|
||||
@ -30,6 +95,32 @@ export interface ExchangeRates {
|
||||
USDCHF: number,
|
||||
USDAUD: number,
|
||||
USDJPY: number,
|
||||
USDBGN?: number,
|
||||
USDBRL?: number,
|
||||
USDCNY?: number,
|
||||
USDCZK?: number,
|
||||
USDDKK?: number,
|
||||
USDHKD?: number,
|
||||
USDHRK?: number,
|
||||
USDHUF?: number,
|
||||
USDIDR?: number,
|
||||
USDILS?: number,
|
||||
USDINR?: number,
|
||||
USDISK?: number,
|
||||
USDKRW?: number,
|
||||
USDMXN?: number,
|
||||
USDMYR?: number,
|
||||
USDNOK?: number,
|
||||
USDNZD?: number,
|
||||
USDPHP?: number,
|
||||
USDPLN?: number,
|
||||
USDRON?: number,
|
||||
USDRUB?: number,
|
||||
USDSEK?: number,
|
||||
USDSGD?: number,
|
||||
USDTHB?: number,
|
||||
USDTRY?: number,
|
||||
USDZAR?: number,
|
||||
}
|
||||
|
||||
export interface Conversion {
|
||||
@ -45,6 +136,32 @@ export const MAX_PRICES = {
|
||||
CHF: 100000000,
|
||||
AUD: 100000000,
|
||||
JPY: 10000000000,
|
||||
BGN: 1000000000,
|
||||
BRL: 1000000000,
|
||||
CNY: 1000000000,
|
||||
CZK: 10000000000,
|
||||
DKK: 1000000000,
|
||||
HKD: 1000000000,
|
||||
HRK: 1000000000,
|
||||
HUF: 10000000000,
|
||||
IDR: 100000000000,
|
||||
ILS: 1000000000,
|
||||
INR: 10000000000,
|
||||
ISK: 10000000000,
|
||||
KRW: 100000000000,
|
||||
MXN: 1000000000,
|
||||
MYR: 1000000000,
|
||||
NOK: 1000000000,
|
||||
NZD: 1000000000,
|
||||
PHP: 10000000000,
|
||||
PLN: 1000000000,
|
||||
RON: 1000000000,
|
||||
RUB: 10000000000,
|
||||
SEK: 1000000000,
|
||||
SGD: 100000000,
|
||||
THB: 10000000000,
|
||||
TRY: 10000000000,
|
||||
ZAR: 10000000000,
|
||||
};
|
||||
|
||||
class PricesRepository {
|
||||
@ -64,17 +181,49 @@ class PricesRepository {
|
||||
}
|
||||
|
||||
try {
|
||||
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]
|
||||
);
|
||||
if (!config.FIAT_PRICE.API_KEY) { // Store only the 7 main currencies
|
||||
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]
|
||||
);
|
||||
} else { // Store all 7 main currencies + all the currencies obtained with the external API
|
||||
await DB.query(`
|
||||
INSERT INTO prices(time, USD, EUR, GBP, CAD, CHF, AUD, JPY, BGN, BRL, CNY, CZK, DKK, HKD, HRK, HUF, IDR, ILS, INR, ISK, KRW, MXN, MYR, NOK, NZD, PHP, PLN, RON, RUB, SEK, SGD, THB, TRY, ZAR)
|
||||
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ? , ?, ?, ?, ?, ?, ?, ?, ? , ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? , ? )`,
|
||||
[time, prices.USD, prices.EUR, prices.GBP, prices.CAD, prices.CHF, prices.AUD, prices.JPY, prices.BGN, prices.BRL, prices.CNY, prices.CZK, prices.DKK,
|
||||
prices.HKD, prices.HRK, prices.HUF, prices.IDR, prices.ILS, prices.INR, prices.ISK, prices.KRW, prices.MXN, prices.MYR, prices.NOK, prices.NZD,
|
||||
prices.PHP, prices.PLN, prices.RON, prices.RUB, prices.SEK, prices.SGD, prices.THB, prices.TRY, prices.ZAR]
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err(`Cannot save exchange rate into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async $saveAdditionalCurrencyPrices(time: number, prices: ApiPrice, legacyCurrencies: string[]): Promise<void> {
|
||||
try {
|
||||
await DB.query(`
|
||||
UPDATE prices
|
||||
SET BGN = ?, BRL = ?, CNY = ?, CZK = ?, DKK = ?, HKD = ?, HRK = ?, HUF = ?, IDR = ?, ILS = ?, INR = ?, ISK = ?, KRW = ?, MXN = ?, MYR = ?, NOK = ?, NZD = ?, PHP = ?, PLN = ?, RON = ?, RUB = ?, SEK = ?, SGD = ?, THB = ?, TRY = ?, ZAR = ?
|
||||
WHERE UNIX_TIMESTAMP(time) = ?`,
|
||||
[prices.BGN, prices.BRL, prices.CNY, prices.CZK, prices.DKK, prices.HKD, prices.HRK, prices.HUF, prices.IDR, prices.ILS, prices.INR, prices.ISK, prices.KRW, prices.MXN, prices.MYR, prices.NOK, prices.NZD, prices.PHP, prices.PLN, prices.RON, prices.RUB, prices.SEK, prices.SGD, prices.THB, prices.TRY, prices.ZAR, time]
|
||||
);
|
||||
for (const currency of legacyCurrencies) {
|
||||
await DB.query(`
|
||||
UPDATE prices
|
||||
SET ${currency} = ?
|
||||
WHERE UNIX_TIMESTAMP(time) = ?`,
|
||||
[prices[currency], time]
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err(`Cannot update exchange rate into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async $getOldestPriceTime(): Promise<number> {
|
||||
const [oldestRow] = await DB.query(`
|
||||
SELECT UNIX_TIMESTAMP(time) AS time
|
||||
@ -118,6 +267,28 @@ class PricesRepository {
|
||||
return times.map(time => time.time);
|
||||
}
|
||||
|
||||
public async $getPricesTimesWithMissingFields(): Promise<{time: number, USD: number, eur_missing: boolean, gbp_missing: boolean, cad_missing: boolean, chf_missing: boolean, aud_missing: boolean, jpy_missing: boolean}[]> {
|
||||
const [times] = await DB.query(`
|
||||
SELECT UNIX_TIMESTAMP(time) AS time,
|
||||
USD,
|
||||
CASE WHEN EUR = -1 THEN TRUE ELSE FALSE END AS eur_missing,
|
||||
CASE WHEN GBP = -1 THEN TRUE ELSE FALSE END AS gbp_missing,
|
||||
CASE WHEN CAD = -1 THEN TRUE ELSE FALSE END AS cad_missing,
|
||||
CASE WHEN CHF = -1 THEN TRUE ELSE FALSE END AS chf_missing,
|
||||
CASE WHEN AUD = -1 THEN TRUE ELSE FALSE END AS aud_missing,
|
||||
CASE WHEN JPY = -1 THEN TRUE ELSE FALSE END AS jpy_missing
|
||||
FROM prices
|
||||
WHERE USD != -1
|
||||
AND -1 IN (EUR, GBP, CAD, CHF, AUD, JPY, BGN, BRL, CNY, CZK, DKK, HKD, HRK, HUF, IDR, ILS, INR, ISK, KRW,
|
||||
MXN, MYR, NOK, NZD, PHP, PLN, RON, RUB, SEK, SGD, THB, TRY, ZAR)
|
||||
ORDER BY time DESC
|
||||
`);
|
||||
if (!Array.isArray(times)) {
|
||||
return [];
|
||||
}
|
||||
return times as {time: number, USD: number, eur_missing: boolean, gbp_missing: boolean, cad_missing: boolean, chf_missing: boolean, aud_missing: boolean, jpy_missing: boolean}[];
|
||||
}
|
||||
|
||||
public async $getPricesTimesAndId(): Promise<{time: number, id: number, USD: number}[]> {
|
||||
const [times] = await DB.query(`
|
||||
SELECT
|
||||
@ -144,7 +315,7 @@ class PricesRepository {
|
||||
return rates[0] as ApiPrice;
|
||||
}
|
||||
|
||||
public async $getNearestHistoricalPrice(timestamp: number | undefined): Promise<Conversion | null> {
|
||||
public async $getNearestHistoricalPrice(timestamp: number | undefined, currency?: string): Promise<Conversion | null> {
|
||||
try {
|
||||
const [rates] = await DB.query(`
|
||||
SELECT ${ApiPriceFields}
|
||||
@ -158,24 +329,91 @@ class PricesRepository {
|
||||
throw Error(`Cannot get single historical price from the database`);
|
||||
}
|
||||
|
||||
const [latestPrices] = await DB.query(`
|
||||
SELECT ${ApiPriceFields}
|
||||
FROM prices
|
||||
ORDER BY time DESC
|
||||
LIMIT 1
|
||||
`);
|
||||
if (!Array.isArray(latestPrices)) {
|
||||
throw Error(`Cannot get single historical price from the database`);
|
||||
}
|
||||
|
||||
// Compute fiat exchange rates
|
||||
let latestPrice = rates[0] as ApiPrice;
|
||||
let latestPrice = latestPrices[0] as ApiPrice;
|
||||
if (!latestPrice || 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;
|
||||
const computeFx = (usd: number, other: number): number => usd <= 0.05 ? 0 : Math.round(Math.max(other, 0) / usd * 100) / 100;
|
||||
|
||||
const exchangeRates: ExchangeRates = {
|
||||
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),
|
||||
const exchangeRates: ExchangeRates = config.FIAT_PRICE.API_KEY ?
|
||||
{
|
||||
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),
|
||||
USDBGN: computeFx(latestPrice.USD, latestPrice.BGN),
|
||||
USDBRL: computeFx(latestPrice.USD, latestPrice.BRL),
|
||||
USDCNY: computeFx(latestPrice.USD, latestPrice.CNY),
|
||||
USDCZK: computeFx(latestPrice.USD, latestPrice.CZK),
|
||||
USDDKK: computeFx(latestPrice.USD, latestPrice.DKK),
|
||||
USDHKD: computeFx(latestPrice.USD, latestPrice.HKD),
|
||||
USDHRK: computeFx(latestPrice.USD, latestPrice.HRK),
|
||||
USDHUF: computeFx(latestPrice.USD, latestPrice.HUF),
|
||||
USDIDR: computeFx(latestPrice.USD, latestPrice.IDR),
|
||||
USDILS: computeFx(latestPrice.USD, latestPrice.ILS),
|
||||
USDINR: computeFx(latestPrice.USD, latestPrice.INR),
|
||||
USDISK: computeFx(latestPrice.USD, latestPrice.ISK),
|
||||
USDKRW: computeFx(latestPrice.USD, latestPrice.KRW),
|
||||
USDMXN: computeFx(latestPrice.USD, latestPrice.MXN),
|
||||
USDMYR: computeFx(latestPrice.USD, latestPrice.MYR),
|
||||
USDNOK: computeFx(latestPrice.USD, latestPrice.NOK),
|
||||
USDNZD: computeFx(latestPrice.USD, latestPrice.NZD),
|
||||
USDPHP: computeFx(latestPrice.USD, latestPrice.PHP),
|
||||
USDPLN: computeFx(latestPrice.USD, latestPrice.PLN),
|
||||
USDRON: computeFx(latestPrice.USD, latestPrice.RON),
|
||||
USDRUB: computeFx(latestPrice.USD, latestPrice.RUB),
|
||||
USDSEK: computeFx(latestPrice.USD, latestPrice.SEK),
|
||||
USDSGD: computeFx(latestPrice.USD, latestPrice.SGD),
|
||||
USDTHB: computeFx(latestPrice.USD, latestPrice.THB),
|
||||
USDTRY: computeFx(latestPrice.USD, latestPrice.TRY),
|
||||
USDZAR: computeFx(latestPrice.USD, latestPrice.ZAR),
|
||||
} : {
|
||||
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),
|
||||
};
|
||||
|
||||
if (currency) {
|
||||
if (!latestPrice[currency]) {
|
||||
return null;
|
||||
}
|
||||
const filteredRates = rates.map((rate: any) => {
|
||||
return {
|
||||
time: rate.time,
|
||||
[currency]: rate[currency],
|
||||
['USD']: rate['USD']
|
||||
};
|
||||
});
|
||||
if (filteredRates.length === 0) { // No price data before 2010-07-19: add a fake entry
|
||||
filteredRates.push({
|
||||
time: 1279497600,
|
||||
[currency]: 0,
|
||||
['USD']: 0
|
||||
});
|
||||
}
|
||||
return {
|
||||
prices: filteredRates as ApiPrice[],
|
||||
exchangeRates: exchangeRates
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
prices: rates as ApiPrice[],
|
||||
exchangeRates: exchangeRates
|
||||
@ -186,7 +424,7 @@ class PricesRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public async $getHistoricalPrices(): Promise<Conversion | null> {
|
||||
public async $getHistoricalPrices(currency?: string): Promise<Conversion | null> {
|
||||
try {
|
||||
const [rates] = await DB.query(`
|
||||
SELECT ${ApiPriceFields}
|
||||
@ -203,18 +441,69 @@ class PricesRepository {
|
||||
latestPrice = priceUpdater.getEmptyPricesObj();
|
||||
}
|
||||
|
||||
const computeFx = (usd: number, other: number): number =>
|
||||
Math.round(Math.max(other, 0) / Math.max(usd, 1) * 100) / 100;
|
||||
const computeFx = (usd: number, other: number): number =>
|
||||
usd <= 0 ? 0 : Math.round(Math.max(other, 0) / usd * 100) / 100;
|
||||
|
||||
const exchangeRates: ExchangeRates = {
|
||||
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),
|
||||
const exchangeRates: ExchangeRates = config.FIAT_PRICE.API_KEY ?
|
||||
{
|
||||
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),
|
||||
USDBGN: computeFx(latestPrice.USD, latestPrice.BGN),
|
||||
USDBRL: computeFx(latestPrice.USD, latestPrice.BRL),
|
||||
USDCNY: computeFx(latestPrice.USD, latestPrice.CNY),
|
||||
USDCZK: computeFx(latestPrice.USD, latestPrice.CZK),
|
||||
USDDKK: computeFx(latestPrice.USD, latestPrice.DKK),
|
||||
USDHKD: computeFx(latestPrice.USD, latestPrice.HKD),
|
||||
USDHRK: computeFx(latestPrice.USD, latestPrice.HRK),
|
||||
USDHUF: computeFx(latestPrice.USD, latestPrice.HUF),
|
||||
USDIDR: computeFx(latestPrice.USD, latestPrice.IDR),
|
||||
USDILS: computeFx(latestPrice.USD, latestPrice.ILS),
|
||||
USDINR: computeFx(latestPrice.USD, latestPrice.INR),
|
||||
USDISK: computeFx(latestPrice.USD, latestPrice.ISK),
|
||||
USDKRW: computeFx(latestPrice.USD, latestPrice.KRW),
|
||||
USDMXN: computeFx(latestPrice.USD, latestPrice.MXN),
|
||||
USDMYR: computeFx(latestPrice.USD, latestPrice.MYR),
|
||||
USDNOK: computeFx(latestPrice.USD, latestPrice.NOK),
|
||||
USDNZD: computeFx(latestPrice.USD, latestPrice.NZD),
|
||||
USDPHP: computeFx(latestPrice.USD, latestPrice.PHP),
|
||||
USDPLN: computeFx(latestPrice.USD, latestPrice.PLN),
|
||||
USDRON: computeFx(latestPrice.USD, latestPrice.RON),
|
||||
USDRUB: computeFx(latestPrice.USD, latestPrice.RUB),
|
||||
USDSEK: computeFx(latestPrice.USD, latestPrice.SEK),
|
||||
USDSGD: computeFx(latestPrice.USD, latestPrice.SGD),
|
||||
USDTHB: computeFx(latestPrice.USD, latestPrice.THB),
|
||||
USDTRY: computeFx(latestPrice.USD, latestPrice.TRY),
|
||||
USDZAR: computeFx(latestPrice.USD, latestPrice.ZAR),
|
||||
} : {
|
||||
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),
|
||||
};
|
||||
|
||||
if (currency) {
|
||||
if (!latestPrice[currency]) {
|
||||
return null;
|
||||
}
|
||||
const filteredRates = rates.map((rate: any) => {
|
||||
return {
|
||||
time: rate.time,
|
||||
[currency]: rate[currency],
|
||||
['USD']: rate['USD']
|
||||
};
|
||||
});
|
||||
return {
|
||||
prices: filteredRates as ApiPrice[],
|
||||
exchangeRates: exchangeRates
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
prices: rates as ApiPrice[],
|
||||
exchangeRates: exchangeRates
|
||||
|
73
backend/src/tasks/price-feeds/free-currency-api.ts
Normal file
73
backend/src/tasks/price-feeds/free-currency-api.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { query } from '../../utils/axios-query';
|
||||
import { ConversionFeed, ConversionRates } from '../price-updater';
|
||||
|
||||
const emptyRates = {
|
||||
AUD: -1,
|
||||
BGN: -1,
|
||||
BRL: -1,
|
||||
CAD: -1,
|
||||
CHF: -1,
|
||||
CNY: -1,
|
||||
CZK: -1,
|
||||
DKK: -1,
|
||||
EUR: -1,
|
||||
GBP: -1,
|
||||
HKD: -1,
|
||||
HRK: -1,
|
||||
HUF: -1,
|
||||
IDR: -1,
|
||||
ILS: -1,
|
||||
INR: -1,
|
||||
ISK: -1,
|
||||
JPY: -1,
|
||||
KRW: -1,
|
||||
MXN: -1,
|
||||
MYR: -1,
|
||||
NOK: -1,
|
||||
NZD: -1,
|
||||
PHP: -1,
|
||||
PLN: -1,
|
||||
RON: -1,
|
||||
RUB: -1,
|
||||
SEK: -1,
|
||||
SGD: -1,
|
||||
THB: -1,
|
||||
TRY: -1,
|
||||
USD: -1,
|
||||
ZAR: -1,
|
||||
};
|
||||
|
||||
class FreeCurrencyApi implements ConversionFeed {
|
||||
private API_KEY: string;
|
||||
|
||||
constructor(apiKey: string) {
|
||||
this.API_KEY = apiKey;
|
||||
}
|
||||
|
||||
public async $getQuota(): Promise<any> {
|
||||
const response = await query(`https://api.freecurrencyapi.com/v1/status?apikey=${this.API_KEY}`);
|
||||
if (response && response['quotas']) {
|
||||
return response['quotas'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async $fetchLatestConversionRates(): Promise<ConversionRates> {
|
||||
const response = await query(`https://api.freecurrencyapi.com/v1/latest?apikey=${this.API_KEY}`);
|
||||
if (response && response['data']) {
|
||||
return response['data'];
|
||||
}
|
||||
return emptyRates;
|
||||
}
|
||||
|
||||
public async $fetchConversionRates(date: string): Promise<ConversionRates> {
|
||||
const response = await query(`https://api.freecurrencyapi.com/v1/historical?date=${date}&apikey=${this.API_KEY}`);
|
||||
if (response && response['data'] && response['data'][date]) {
|
||||
return response['data'][date];
|
||||
}
|
||||
return emptyRates;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default FreeCurrencyApi;
|
@ -8,6 +8,7 @@ import BitflyerApi from './price-feeds/bitflyer-api';
|
||||
import CoinbaseApi from './price-feeds/coinbase-api';
|
||||
import GeminiApi from './price-feeds/gemini-api';
|
||||
import KrakenApi from './price-feeds/kraken-api';
|
||||
import FreeCurrencyApi from './price-feeds/free-currency-api';
|
||||
|
||||
export interface PriceFeed {
|
||||
name: string;
|
||||
@ -23,6 +24,16 @@ export interface PriceHistory {
|
||||
[timestamp: number]: ApiPrice;
|
||||
}
|
||||
|
||||
export interface ConversionFeed {
|
||||
$getQuota(): Promise<any>;
|
||||
$fetchLatestConversionRates(): Promise<ConversionRates>;
|
||||
$fetchConversionRates(date: string): Promise<ConversionRates>;
|
||||
}
|
||||
|
||||
export interface ConversionRates {
|
||||
[currency: string]: number
|
||||
}
|
||||
|
||||
function getMedian(arr: number[]): number {
|
||||
const sortedArr = arr.slice().sort((a, b) => a - b);
|
||||
const mid = Math.floor(sortedArr.length / 2);
|
||||
@ -33,6 +44,9 @@ function getMedian(arr: number[]): number {
|
||||
|
||||
class PriceUpdater {
|
||||
public historyInserted = false;
|
||||
private additionalCurrenciesHistoryInserted = false;
|
||||
private additionalCurrenciesHistoryRunning = false;
|
||||
private lastFailedHistoricalRun = 0;
|
||||
private timeBetweenUpdatesMs = 360_0000 / config.MEMPOOL.PRICE_UPDATES_PER_HOUR;
|
||||
private cyclePosition = -1;
|
||||
private firstRun = true;
|
||||
@ -42,6 +56,10 @@ class PriceUpdater {
|
||||
private feeds: PriceFeed[] = [];
|
||||
private currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY'];
|
||||
private latestPrices: ApiPrice;
|
||||
private currencyConversionFeed: ConversionFeed | undefined;
|
||||
private newCurrencies: string[] = ['BGN', 'BRL', 'CNY', 'CZK', 'DKK', 'HKD', 'HRK', 'HUF', 'IDR', 'ILS', 'INR', 'ISK', 'KRW', 'MXN', 'MYR', 'NOK', 'NZD', 'PHP', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'THB', 'TRY', 'ZAR'];
|
||||
private lastTimeConversionsRatesFetched: number = 0;
|
||||
private latestConversionsRatesFromFeed: ConversionRates = {};
|
||||
private ratesChangedCallback: ((rates: ApiPrice) => void) | undefined;
|
||||
|
||||
constructor() {
|
||||
@ -53,6 +71,7 @@ class PriceUpdater {
|
||||
this.feeds.push(new BitfinexApi());
|
||||
this.feeds.push(new GeminiApi());
|
||||
|
||||
this.currencyConversionFeed = new FreeCurrencyApi(config.FIAT_PRICE.API_KEY);
|
||||
this.setCyclePosition();
|
||||
}
|
||||
|
||||
@ -70,6 +89,32 @@ class PriceUpdater {
|
||||
CHF: -1,
|
||||
AUD: -1,
|
||||
JPY: -1,
|
||||
BGN: -1,
|
||||
BRL: -1,
|
||||
CNY: -1,
|
||||
CZK: -1,
|
||||
DKK: -1,
|
||||
HKD: -1,
|
||||
HRK: -1,
|
||||
HUF: -1,
|
||||
IDR: -1,
|
||||
ILS: -1,
|
||||
INR: -1,
|
||||
ISK: -1,
|
||||
KRW: -1,
|
||||
MXN: -1,
|
||||
MYR: -1,
|
||||
NOK: -1,
|
||||
NZD: -1,
|
||||
PHP: -1,
|
||||
PLN: -1,
|
||||
RON: -1,
|
||||
RUB: -1,
|
||||
SEK: -1,
|
||||
SGD: -1,
|
||||
THB: -1,
|
||||
TRY: -1,
|
||||
ZAR: -1,
|
||||
};
|
||||
}
|
||||
|
||||
@ -99,6 +144,23 @@ class PriceUpdater {
|
||||
if ((Math.round(new Date().getTime() / 1000) - this.lastHistoricalRun) > 3600 * 24) {
|
||||
// Once a day, look for missing prices (could happen due to network connectivity issues)
|
||||
this.historyInserted = false;
|
||||
this.additionalCurrenciesHistoryInserted = false;
|
||||
}
|
||||
|
||||
if (this.lastFailedHistoricalRun > 0 && (Math.round(new Date().getTime() / 1000) - this.lastFailedHistoricalRun) > 60) {
|
||||
// If the last attempt to insert missing prices failed, we try again after 60 seconds
|
||||
this.additionalCurrenciesHistoryInserted = false;
|
||||
}
|
||||
|
||||
if (config.FIAT_PRICE.API_KEY && this.currencyConversionFeed && (Math.round(new Date().getTime() / 1000) - this.lastTimeConversionsRatesFetched) > 3600 * 24) {
|
||||
// Once a day, fetch conversion rates from api: we don't need more granularity for fiat currencies and have a limited number of requests
|
||||
try {
|
||||
this.latestConversionsRatesFromFeed = await this.currencyConversionFeed.$fetchLatestConversionRates();
|
||||
this.lastTimeConversionsRatesFetched = Math.round(new Date().getTime() / 1000);
|
||||
logger.debug(`Fetched currencies conversion rates from external API: ${JSON.stringify(this.latestConversionsRatesFromFeed)}`);
|
||||
} catch (e) {
|
||||
logger.err(`Cannot fetch conversion rates from the API. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@ -106,6 +168,10 @@ class PriceUpdater {
|
||||
if (this.historyInserted === false && config.DATABASE.ENABLED === true) {
|
||||
await this.$insertHistoricalPrices();
|
||||
}
|
||||
if (this.additionalCurrenciesHistoryInserted === false && config.DATABASE.ENABLED === true && config.FIAT_PRICE.API_KEY && !this.additionalCurrenciesHistoryRunning) {
|
||||
await this.$insertMissingAdditionalPrices();
|
||||
}
|
||||
|
||||
} catch (e: any) {
|
||||
logger.err(`Cannot save BTC prices in db. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
|
||||
}
|
||||
@ -185,6 +251,14 @@ class PriceUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
if (config.FIAT_PRICE.API_KEY && this.latestPrices.USD > 0 && Object.keys(this.latestConversionsRatesFromFeed).length > 0) {
|
||||
for (const conversionCurrency of this.newCurrencies) {
|
||||
if (this.latestConversionsRatesFromFeed[conversionCurrency] > 0 && this.latestPrices.USD * this.latestConversionsRatesFromFeed[conversionCurrency] < MAX_PRICES[conversionCurrency]) {
|
||||
this.latestPrices[conversionCurrency] = Math.round(this.latestPrices.USD * this.latestConversionsRatesFromFeed[conversionCurrency]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.DATABASE.ENABLED === true && this.cyclePosition === 0) {
|
||||
// Save everything in db
|
||||
try {
|
||||
@ -253,7 +327,7 @@ class PriceUpdater {
|
||||
await this.$insertMissingRecentPrices('hour');
|
||||
|
||||
this.historyInserted = true;
|
||||
this.lastHistoricalRun = new Date().getTime();
|
||||
this.lastHistoricalRun = Math.round(new Date().getTime() / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -320,6 +394,83 @@ class PriceUpdater {
|
||||
logger.debug(`Inserted ${totalInserted} ${type === 'day' ? 'dai' : 'hour'}ly historical prices into the db`, logger.tags.mining);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find missing prices for additional currencies and insert them in the database
|
||||
* We calculate the additional prices from the USD price and the conversion rates
|
||||
*/
|
||||
private async $insertMissingAdditionalPrices(): Promise<void> {
|
||||
this.lastFailedHistoricalRun = 0;
|
||||
const priceTimesToFill = await PricesRepository.$getPricesTimesWithMissingFields();
|
||||
if (priceTimesToFill.length === 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const remainingQuota = await this.currencyConversionFeed?.$getQuota();
|
||||
if (remainingQuota['month']['remaining'] < 500) { // We need some calls left for the daily updates
|
||||
logger.debug(`Not enough currency API credit to insert missing prices in ${priceTimesToFill.length} rows (${remainingQuota['month']['remaining']} calls left).`, logger.tags.mining);
|
||||
this.additionalCurrenciesHistoryInserted = true; // Do not try again until next day
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err(`Cannot fetch currency API credit, insertion of missing prices aborted. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.additionalCurrenciesHistoryRunning = true;
|
||||
logger.debug(`Fetching missing conversion rates from external API to fill ${priceTimesToFill.length} rows`, logger.tags.mining);
|
||||
|
||||
let conversionRates: { [timestamp: number]: ConversionRates } = {};
|
||||
let totalInserted = 0;
|
||||
|
||||
for (let i = 0; i < priceTimesToFill.length; i++) {
|
||||
const priceTime = priceTimesToFill[i];
|
||||
const missingLegacyCurrencies = this.getMissingLegacyCurrencies(priceTime); // In the case a legacy currency (EUR, GBP, CAD, CHF, AUD, JPY)
|
||||
const year = new Date(priceTime.time * 1000).getFullYear(); // is missing, we use the same process as for the new currencies
|
||||
const month = new Date(priceTime.time * 1000).getMonth();
|
||||
const yearMonthTimestamp = new Date(year, month, 1).getTime() / 1000;
|
||||
if (conversionRates[yearMonthTimestamp] === undefined) {
|
||||
conversionRates[yearMonthTimestamp] = await this.currencyConversionFeed?.$fetchConversionRates(`${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-01`) || { USD: -1 };
|
||||
if (conversionRates[yearMonthTimestamp]['USD'] < 0) {
|
||||
logger.err(`Cannot fetch conversion rates from the API for ${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-01. Aborting insertion of missing prices.`, logger.tags.mining);
|
||||
this.lastFailedHistoricalRun = Math.round(new Date().getTime() / 1000);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const prices: ApiPrice = this.getEmptyPricesObj();
|
||||
|
||||
let willInsert = false;
|
||||
for (const conversionCurrency of this.newCurrencies.concat(missingLegacyCurrencies)) {
|
||||
if (conversionRates[yearMonthTimestamp][conversionCurrency] > 0 && priceTime.USD * conversionRates[yearMonthTimestamp][conversionCurrency] < MAX_PRICES[conversionCurrency]) {
|
||||
prices[conversionCurrency] = year >= 2013 ? Math.round(priceTime.USD * conversionRates[yearMonthTimestamp][conversionCurrency]) : Math.round(priceTime.USD * conversionRates[yearMonthTimestamp][conversionCurrency] * 100) / 100;
|
||||
willInsert = true;
|
||||
} else {
|
||||
prices[conversionCurrency] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (willInsert) {
|
||||
await PricesRepository.$saveAdditionalCurrencyPrices(priceTime.time, prices, missingLegacyCurrencies);
|
||||
++totalInserted;
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(`Inserted ${totalInserted} missing additional currency prices into the db`, logger.tags.mining);
|
||||
this.additionalCurrenciesHistoryInserted = true;
|
||||
this.additionalCurrenciesHistoryRunning = false;
|
||||
}
|
||||
|
||||
// Helper function to get legacy missing currencies in a row (EUR, GBP, CAD, CHF, AUD, JPY)
|
||||
private getMissingLegacyCurrencies(priceTime: any): string[] {
|
||||
const missingCurrencies: string[] = [];
|
||||
['eur', 'gbp', 'cad', 'chf', 'aud', 'jpy'].forEach(currency => {
|
||||
if (priceTime[`${currency}_missing`]) {
|
||||
missingCurrencies.push(currency.toUpperCase());
|
||||
}
|
||||
});
|
||||
return missingCurrencies;
|
||||
}
|
||||
}
|
||||
|
||||
export default new PriceUpdater();
|
||||
|
@ -153,5 +153,9 @@
|
||||
"ENABLED": __REDIS_ENABLED__,
|
||||
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__",
|
||||
"BATCH_QUERY_BASE_SIZE": __REDIS_BATCH_QUERY_BASE_SIZE__
|
||||
},
|
||||
"FIAT_PRICE": {
|
||||
"ENABLED": __FIAT_PRICE_ENABLED__,
|
||||
"API_KEY": "__FIAT_PRICE_API_KEY__"
|
||||
}
|
||||
}
|
||||
|
@ -155,6 +155,10 @@ __REDIS_ENABLED__=${REDIS_ENABLED:=false}
|
||||
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
|
||||
__REDIS_BATCH_QUERY_BASE_SIZE__=${REDIS_BATCH_QUERY_BASE_SIZE:=5000}
|
||||
|
||||
# FIAT_PRICE
|
||||
__FIAT_PRICE_ENABLED__=${FIAT_PRICE_ENABLED:=true}
|
||||
__FIAT_PRICE_API_KEY__=${FIAT_PRICE_API_KEY:=""}
|
||||
|
||||
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
||||
|
||||
sed -i "s!__MEMPOOL_NETWORK__!${__MEMPOOL_NETWORK__}!g" mempool-config.json
|
||||
@ -301,4 +305,8 @@ sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json
|
||||
sed -i "s!__REDIS_BATCH_QUERY_BASE_SIZE__!${__REDIS_BATCH_QUERY_BASE_SIZE__}!g" mempool-config.json
|
||||
|
||||
# FIAT_PRICE
|
||||
sed -i "s!__FIAT_PRICE_ENABLED__!${__FIAT_PRICE_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__FIAT_PRICE_API_KEY__!${__FIAT_PRICE_API_KEY__}!g" mempool-config.json
|
||||
|
||||
node /backend/package/index.js
|
||||
|
@ -268,4 +268,134 @@ export const fiatCurrencies = {
|
||||
code: 'USD',
|
||||
indexed: true,
|
||||
},
|
||||
BGN: {
|
||||
name: 'Bulgarian Lev',
|
||||
code: 'BGN',
|
||||
indexed: true,
|
||||
},
|
||||
BRL: {
|
||||
name: 'Brazilian Real',
|
||||
code: 'BRL',
|
||||
indexed: true,
|
||||
},
|
||||
CNY: {
|
||||
name: 'Chinese Yuan',
|
||||
code: 'CNY',
|
||||
indexed: true,
|
||||
},
|
||||
CZK: {
|
||||
name: 'Czech Koruna',
|
||||
code: 'CZK',
|
||||
indexed: true,
|
||||
},
|
||||
DKK: {
|
||||
name: 'Danish Krone',
|
||||
code: 'DKK',
|
||||
indexed: true,
|
||||
},
|
||||
HKD: {
|
||||
name: 'Hong Kong Dollar',
|
||||
code: 'HKD',
|
||||
indexed: true,
|
||||
},
|
||||
HRK: {
|
||||
name: 'Croatian Kuna',
|
||||
code: 'HRK',
|
||||
indexed: true,
|
||||
},
|
||||
HUF: {
|
||||
name: 'Hungarian Forint',
|
||||
code: 'HUF',
|
||||
indexed: true,
|
||||
},
|
||||
IDR: {
|
||||
name: 'Indonesian Rupiah',
|
||||
code: 'IDR',
|
||||
indexed: true,
|
||||
},
|
||||
ILS: {
|
||||
name: 'Israeli Shekel',
|
||||
code: 'ILS',
|
||||
indexed: true,
|
||||
},
|
||||
INR: {
|
||||
name: 'Indian Rupee',
|
||||
code: 'INR',
|
||||
indexed: true,
|
||||
},
|
||||
ISK: {
|
||||
name: 'Icelandic Krona',
|
||||
code: 'ISK',
|
||||
indexed: true,
|
||||
},
|
||||
KRW: {
|
||||
name: 'South Korean Won',
|
||||
code: 'KRW',
|
||||
indexed: true,
|
||||
},
|
||||
MXN: {
|
||||
name: 'Mexican Peso',
|
||||
code: 'MXN',
|
||||
indexed: true,
|
||||
},
|
||||
MYR: {
|
||||
name: 'Malaysian Ringgit',
|
||||
code: 'MYR',
|
||||
indexed: true,
|
||||
},
|
||||
NOK: {
|
||||
name: 'Norwegian Krone',
|
||||
code: 'NOK',
|
||||
indexed: true,
|
||||
},
|
||||
NZD: {
|
||||
name: 'New Zealand Dollar',
|
||||
code: 'NZD',
|
||||
indexed: true,
|
||||
},
|
||||
PHP: {
|
||||
name: 'Philippine Peso',
|
||||
code: 'PHP',
|
||||
indexed: true,
|
||||
},
|
||||
PLN: {
|
||||
name: 'Polish Zloty',
|
||||
code: 'PLN',
|
||||
indexed: true,
|
||||
},
|
||||
RON: {
|
||||
name: 'Romanian Leu',
|
||||
code: 'RON',
|
||||
indexed: true,
|
||||
},
|
||||
RUB: {
|
||||
name: 'Russian Ruble',
|
||||
code: 'RUB',
|
||||
indexed: true,
|
||||
},
|
||||
SEK: {
|
||||
name: 'Swedish Krona',
|
||||
code: 'SEK',
|
||||
indexed: true,
|
||||
},
|
||||
SGD: {
|
||||
name: 'Singapore Dollar',
|
||||
code: 'SGD',
|
||||
indexed: true,
|
||||
},
|
||||
THB: {
|
||||
name: 'Thai Baht',
|
||||
code: 'THB',
|
||||
indexed: true,
|
||||
},
|
||||
TRY: {
|
||||
name: 'Turkish Lira',
|
||||
code: 'TRY',
|
||||
indexed: true,
|
||||
},
|
||||
ZAR: {
|
||||
name: 'South African Rand',
|
||||
code: 'ZAR',
|
||||
indexed: true,
|
||||
},
|
||||
};
|
@ -335,7 +335,7 @@
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<app-transactions-list [transactions]="transactions" [paginated]="true"></app-transactions-list>
|
||||
<app-transactions-list [transactions]="transactions" [paginated]="true" [blockTime]="block.timestamp"></app-transactions-list>
|
||||
|
||||
<ng-template [ngIf]="transactionsError">
|
||||
<div class="text-center">
|
||||
|
@ -533,9 +533,9 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
if (this.priceSubscription) {
|
||||
this.priceSubscription.unsubscribe();
|
||||
}
|
||||
this.priceSubscription = block$.pipe(
|
||||
switchMap((block) => {
|
||||
return this.priceService.getBlockPrice$(block.timestamp).pipe(
|
||||
this.priceSubscription = combineLatest([this.stateService.fiatCurrency$, block$]).pipe(
|
||||
switchMap(([currency, block]) => {
|
||||
return this.priceService.getBlockPrice$(block.timestamp, true, currency).pipe(
|
||||
tap((price) => {
|
||||
this.blockConversion = price;
|
||||
})
|
||||
|
@ -35,6 +35,11 @@ export class FiatSelectorComponent implements OnInit {
|
||||
this.stateService.fiatCurrency$.subscribe((fiat) => {
|
||||
this.fiatForm.get('fiat')?.setValue(fiat);
|
||||
});
|
||||
if (!this.stateService.env.ADDITIONAL_CURRENCIES) {
|
||||
this.currencies = this.currencies.filter((currency: any) => {
|
||||
return ['AUD', 'CAD', 'EUR', 'JPY', 'GBP', 'CHF', 'USD'].includes(currency[0]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
changeFiat() {
|
||||
|
@ -533,7 +533,7 @@
|
||||
<tr *ngIf="isMobile && (network === 'liquid' || network === 'liquidtestnet' || !featuresEnabled || network === '')"></tr>
|
||||
<tr>
|
||||
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||
<td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [blockConversion]="blockConversion" [value]="tx.fee"></app-fiat></span></td>
|
||||
<td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee"></app-fiat></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
|
||||
|
@ -76,6 +76,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
mempoolBlocksSubscription: Subscription;
|
||||
blocksSubscription: Subscription;
|
||||
miningSubscription: Subscription;
|
||||
currencyChangeSubscription: Subscription;
|
||||
fragmentParams: URLSearchParams;
|
||||
rbfTransaction: undefined | Transaction;
|
||||
replaced: boolean = false;
|
||||
@ -108,7 +109,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
hideFlow: boolean = this.stateService.hideFlow.value;
|
||||
overrideFlowPreference: boolean = null;
|
||||
flowEnabled: boolean;
|
||||
blockConversion: Price;
|
||||
tooltipPosition: { x: number, y: number };
|
||||
isMobile: boolean;
|
||||
|
||||
@ -493,10 +493,12 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
this.fetchRbfHistory$.next(this.tx.txid);
|
||||
|
||||
this.priceService.getBlockPrice$(tx.status?.block_time, true).pipe(
|
||||
tap((price) => {
|
||||
this.blockConversion = price;
|
||||
this.currencyChangeSubscription?.unsubscribe();
|
||||
this.currencyChangeSubscription = this.stateService.fiatCurrency$.pipe(
|
||||
switchMap((currency) => {
|
||||
return tx.status.block_time ? this.priceService.getBlockPrice$(tx.status.block_time, true, currency).pipe(
|
||||
tap((price) => tx['price'] = price),
|
||||
) : of(undefined);
|
||||
})
|
||||
).subscribe();
|
||||
|
||||
@ -810,6 +812,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.mempoolBlocksSubscription.unsubscribe();
|
||||
this.blocksSubscription.unsubscribe();
|
||||
this.miningSubscription?.unsubscribe();
|
||||
this.currencyChangeSubscription?.unsubscribe();
|
||||
this.leaveTransaction();
|
||||
}
|
||||
}
|
||||
|
@ -32,11 +32,14 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
@Input() outputIndex: number;
|
||||
@Input() address: string = '';
|
||||
@Input() rowLimit = 12;
|
||||
@Input() blockTime: number = 0; // Used for price calculation if all the transactions are in the same block
|
||||
|
||||
@Output() loadMore = new EventEmitter();
|
||||
|
||||
latestBlock$: Observable<BlockExtended>;
|
||||
outspendsSubscription: Subscription;
|
||||
currencyChangeSubscription: Subscription;
|
||||
currency: string;
|
||||
refreshOutspends$: ReplaySubject<string[]> = new ReplaySubject();
|
||||
refreshChannels$: ReplaySubject<string[]> = new ReplaySubject();
|
||||
showDetails$ = new BehaviorSubject<boolean>(false);
|
||||
@ -125,6 +128,35 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
)
|
||||
,
|
||||
).subscribe(() => this.ref.markForCheck());
|
||||
|
||||
this.currencyChangeSubscription = this.stateService.fiatCurrency$
|
||||
.subscribe(currency => {
|
||||
this.currency = currency;
|
||||
this.refreshPrice();
|
||||
});
|
||||
}
|
||||
|
||||
refreshPrice(): void {
|
||||
// Loop over all transactions
|
||||
if (!this.transactions || !this.transactions.length || !this.currency) {
|
||||
return;
|
||||
}
|
||||
const confirmedTxs = this.transactions.filter((tx) => tx.status.confirmed).length;
|
||||
if (!this.blockTime) {
|
||||
this.transactions.forEach((tx) => {
|
||||
if (!this.blockTime) {
|
||||
if (tx.status.block_time) {
|
||||
this.priceService.getBlockPrice$(tx.status.block_time, confirmedTxs < 10, this.currency).pipe(
|
||||
tap((price) => tx['price'] = price),
|
||||
).subscribe();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.priceService.getBlockPrice$(this.blockTime, true, this.currency).pipe(
|
||||
tap((price) => this.transactions.forEach((tx) => tx['price'] = price)),
|
||||
).subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes): void {
|
||||
@ -148,6 +180,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
this.transactionsLength = this.transactions.length;
|
||||
this.cacheService.setTxCache(this.transactions);
|
||||
|
||||
const confirmedTxs = this.transactions.filter((tx) => tx.status.confirmed).length;
|
||||
this.transactions.forEach((tx) => {
|
||||
tx['@voutLimit'] = true;
|
||||
tx['@vinLimit'] = true;
|
||||
@ -197,10 +230,18 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
this.priceService.getBlockPrice$(tx.status.block_time).pipe(
|
||||
tap((price) => tx['price'] = price)
|
||||
).subscribe();
|
||||
if (!this.blockTime && tx.status.block_time && this.currency) {
|
||||
this.priceService.getBlockPrice$(tx.status.block_time, confirmedTxs < 10, this.currency).pipe(
|
||||
tap((price) => tx['price'] = price),
|
||||
).subscribe();
|
||||
}
|
||||
});
|
||||
|
||||
if (this.blockTime && this.transactions?.length && this.currency) {
|
||||
this.priceService.getBlockPrice$(this.blockTime, true, this.currency).pipe(
|
||||
tap((price) => this.transactions.forEach((tx) => tx['price'] = price)),
|
||||
).subscribe();
|
||||
}
|
||||
const txIds = this.transactions.filter((tx) => !tx._outspends).map((tx) => tx.txid);
|
||||
if (txIds.length && !this.cached) {
|
||||
this.refreshOutspends$.next(txIds);
|
||||
@ -308,5 +349,6 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.outspendsSubscription.unsubscribe();
|
||||
this.currencyChangeSubscription?.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Component, ElementRef, ViewChild, Input, OnChanges, OnInit } from '@angular/core';
|
||||
import { tap } from 'rxjs';
|
||||
import { Subscription, of, switchMap, tap } from 'rxjs';
|
||||
import { Price, PriceService } from '../../services/price.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { environment } from '../../../environments/environment';
|
||||
@ -35,6 +35,7 @@ export class TxBowtieGraphTooltipComponent implements OnChanges {
|
||||
|
||||
tooltipPosition = { x: 0, y: 0 };
|
||||
blockConversion: Price;
|
||||
currencyChangeSubscription: Subscription;
|
||||
|
||||
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
|
||||
|
||||
@ -47,11 +48,14 @@ export class TxBowtieGraphTooltipComponent implements OnChanges {
|
||||
|
||||
ngOnChanges(changes): void {
|
||||
if (changes.line?.currentValue) {
|
||||
this.priceService.getBlockPrice$(changes.line?.currentValue.timestamp, true).pipe(
|
||||
tap((price) => {
|
||||
this.blockConversion = price;
|
||||
})
|
||||
).subscribe();
|
||||
this.currencyChangeSubscription?.unsubscribe();
|
||||
this.currencyChangeSubscription = this.stateService.fiatCurrency$.pipe(
|
||||
switchMap((currency) => {
|
||||
return changes.line?.currentValue.timestamp ? this.priceService.getBlockPrice$(changes.line?.currentValue.timestamp, true, currency).pipe(
|
||||
tap((price) => this.blockConversion = price),
|
||||
) : of(undefined);
|
||||
})
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
if (changes.cursorPosition && changes.cursorPosition.currentValue) {
|
||||
|
@ -405,7 +405,7 @@ export class ApiService {
|
||||
);
|
||||
}
|
||||
|
||||
getHistoricalPrice$(timestamp: number | undefined): Observable<Conversion> {
|
||||
getHistoricalPrice$(timestamp: number | undefined, currency?: string): Observable<Conversion> {
|
||||
if (this.stateService.isAnyTestnet()) {
|
||||
return of({
|
||||
prices: [],
|
||||
@ -416,12 +416,47 @@ export class ApiService {
|
||||
USDCHF: 0,
|
||||
USDAUD: 0,
|
||||
USDJPY: 0,
|
||||
USDBGN: 0,
|
||||
USDBRL: 0,
|
||||
USDCNY: 0,
|
||||
USDCZK: 0,
|
||||
USDDKK: 0,
|
||||
USDHKD: 0,
|
||||
USDHRK: 0,
|
||||
USDHUF: 0,
|
||||
USDIDR: 0,
|
||||
USDILS: 0,
|
||||
USDINR: 0,
|
||||
USDISK: 0,
|
||||
USDKRW: 0,
|
||||
USDMXN: 0,
|
||||
USDMYR: 0,
|
||||
USDNOK: 0,
|
||||
USDNZD: 0,
|
||||
USDPHP: 0,
|
||||
USDPLN: 0,
|
||||
USDRON: 0,
|
||||
USDRUB: 0,
|
||||
USDSEK: 0,
|
||||
USDSGD: 0,
|
||||
USDTHB: 0,
|
||||
USDTRY: 0,
|
||||
USDZAR: 0,
|
||||
}
|
||||
});
|
||||
}
|
||||
const queryParams = [];
|
||||
|
||||
if (timestamp) {
|
||||
queryParams.push(`timestamp=${timestamp}`);
|
||||
}
|
||||
|
||||
if (currency) {
|
||||
queryParams.push(`currency=${currency}`);
|
||||
}
|
||||
return this.httpClient.get<Conversion>(
|
||||
this.apiBaseUrl + this.apiBasePath + '/api/v1/historical-price' +
|
||||
(timestamp ? `?timestamp=${timestamp}` : '')
|
||||
`${this.apiBaseUrl}${this.apiBasePath}/api/v1/historical-price` +
|
||||
(queryParams.length > 0 ? `?${queryParams.join('&')}` : '')
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,32 @@ export interface ApiPrice {
|
||||
CHF: number,
|
||||
AUD: number,
|
||||
JPY: number,
|
||||
BGN?: number,
|
||||
BRL?: number,
|
||||
CNY?: number,
|
||||
CZK?: number,
|
||||
DKK?: number,
|
||||
HKD?: number,
|
||||
HRK?: number,
|
||||
HUF?: number,
|
||||
IDR?: number,
|
||||
ILS?: number,
|
||||
INR?: number,
|
||||
ISK?: number,
|
||||
KRW?: number,
|
||||
MXN?: number,
|
||||
MYR?: number,
|
||||
NOK?: number,
|
||||
NZD?: number,
|
||||
PHP?: number,
|
||||
PLN?: number,
|
||||
RON?: number,
|
||||
RUB?: number,
|
||||
SEK?: number,
|
||||
SGD?: number,
|
||||
THB?: number,
|
||||
TRY?: number,
|
||||
ZAR?: number,
|
||||
}
|
||||
export interface ExchangeRates {
|
||||
USDEUR: number,
|
||||
@ -21,6 +47,32 @@ export interface ExchangeRates {
|
||||
USDCHF: number,
|
||||
USDAUD: number,
|
||||
USDJPY: number,
|
||||
USDBGN?: number,
|
||||
USDBRL?: number,
|
||||
USDCNY?: number,
|
||||
USDCZK?: number,
|
||||
USDDKK?: number,
|
||||
USDHKD?: number,
|
||||
USDHRK?: number,
|
||||
USDHUF?: number,
|
||||
USDIDR?: number,
|
||||
USDILS?: number,
|
||||
USDINR?: number,
|
||||
USDISK?: number,
|
||||
USDKRW?: number,
|
||||
USDMXN?: number,
|
||||
USDMYR?: number,
|
||||
USDNOK?: number,
|
||||
USDNZD?: number,
|
||||
USDPHP?: number,
|
||||
USDPLN?: number,
|
||||
USDRON?: number,
|
||||
USDRUB?: number,
|
||||
USDSEK?: number,
|
||||
USDSGD?: number,
|
||||
USDTHB?: number,
|
||||
USDTRY?: number,
|
||||
USDZAR?: number,
|
||||
}
|
||||
export interface Conversion {
|
||||
prices: ApiPrice[],
|
||||
@ -46,6 +98,8 @@ export class PriceService {
|
||||
|
||||
lastQueriedTimestamp: number;
|
||||
lastPriceHistoryUpdate: number;
|
||||
lastQueriedCurrency: string;
|
||||
lastQueriedHistoricalCurrency: string;
|
||||
|
||||
historicalPrice: ConversionDict = {
|
||||
prices: null,
|
||||
@ -60,16 +114,25 @@ export class PriceService {
|
||||
|
||||
getEmptyPrice(): Price {
|
||||
return {
|
||||
price: {
|
||||
price: this.stateService.env.ADDITIONAL_CURRENCIES ? {
|
||||
USD: 0, EUR: 0, GBP: 0, CAD: 0, CHF: 0, AUD: 0, JPY: 0, BGN: 0, BRL: 0, CNY: 0, CZK: 0, DKK: 0, HKD: 0, HRK: 0, HUF: 0, IDR: 0,
|
||||
ILS: 0, INR: 0, ISK: 0, KRW: 0, MXN: 0, MYR: 0, NOK: 0, NZD: 0, PHP: 0, PLN: 0, RON: 0, RUB: 0, SEK: 0, SGD: 0, THB: 0, TRY: 0,
|
||||
ZAR: 0
|
||||
} :
|
||||
{
|
||||
USD: 0, EUR: 0, GBP: 0, CAD: 0, CHF: 0, AUD: 0, JPY: 0,
|
||||
},
|
||||
exchangeRates: {
|
||||
exchangeRates: this.stateService.env.ADDITIONAL_CURRENCIES ? {
|
||||
USDEUR: 0, USDGBP: 0, USDCAD: 0, USDCHF: 0, USDAUD: 0, USDJPY: 0, USDBGN: 0, USDBRL: 0, USDCNY: 0, USDCZK: 0, USDDKK: 0, USDHKD: 0,
|
||||
USDHRK: 0, USDHUF: 0, USDIDR: 0, USDILS: 0, USDINR: 0, USDISK: 0, USDKRW: 0, USDMXN: 0, USDMYR: 0, USDNOK: 0, USDNZD: 0, USDPHP: 0,
|
||||
USDPLN: 0, USDRON: 0, USDRUB: 0, USDSEK: 0, USDSGD: 0, USDTHB: 0, USDTRY: 0, USDZAR: 0
|
||||
} : {
|
||||
USDEUR: 0, USDGBP: 0, USDCAD: 0, USDCHF: 0, USDAUD: 0, USDJPY: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getBlockPrice$(blockTimestamp: number, singlePrice = false): Observable<Price | undefined> {
|
||||
getBlockPrice$(blockTimestamp: number, singlePrice = false, currency: string): Observable<Price | undefined> {
|
||||
if (this.stateService.env.BASE_MODULE !== 'mempool' || !this.stateService.env.HISTORICAL_PRICE) {
|
||||
return of(undefined);
|
||||
}
|
||||
@ -81,9 +144,10 @@ export class PriceService {
|
||||
* 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());
|
||||
if (!this.singlePriceObservable$ || (this.singlePriceObservable$ && (blockTimestamp !== this.lastQueriedTimestamp || currency !== this.lastQueriedCurrency))) {
|
||||
this.singlePriceObservable$ = this.apiService.getHistoricalPrice$(blockTimestamp, currency).pipe(shareReplay());
|
||||
this.lastQueriedTimestamp = blockTimestamp;
|
||||
this.lastQueriedCurrency = currency;
|
||||
}
|
||||
|
||||
return this.singlePriceObservable$.pipe(
|
||||
@ -92,7 +156,17 @@ export class PriceService {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
price: {
|
||||
price: this.stateService.env.ADDITIONAL_CURRENCIES ? {
|
||||
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, BGN: conversion.prices[0].BGN,
|
||||
BRL: conversion.prices[0].BRL, CNY: conversion.prices[0].CNY, CZK: conversion.prices[0].CZK, DKK: conversion.prices[0].DKK,
|
||||
HKD: conversion.prices[0].HKD, HRK: conversion.prices[0].HRK, HUF: conversion.prices[0].HUF, IDR: conversion.prices[0].IDR,
|
||||
ILS: conversion.prices[0].ILS, INR: conversion.prices[0].INR, ISK: conversion.prices[0].ISK, KRW: conversion.prices[0].KRW,
|
||||
MXN: conversion.prices[0].MXN, MYR: conversion.prices[0].MYR, NOK: conversion.prices[0].NOK, NZD: conversion.prices[0].NZD,
|
||||
PHP: conversion.prices[0].PHP, PLN: conversion.prices[0].PLN, RON: conversion.prices[0].RON, RUB: conversion.prices[0].RUB,
|
||||
SEK: conversion.prices[0].SEK, SGD: conversion.prices[0].SGD, THB: conversion.prices[0].THB, TRY: conversion.prices[0].TRY,
|
||||
ZAR: conversion.prices[0].ZAR
|
||||
} : {
|
||||
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
|
||||
},
|
||||
@ -106,9 +180,10 @@ export class PriceService {
|
||||
* Query all price history only once. The observable is invalidated after 1 hour
|
||||
*/
|
||||
else {
|
||||
if (!this.priceObservable$ || (this.priceObservable$ && (now - this.lastPriceHistoryUpdate > 3600))) {
|
||||
this.priceObservable$ = this.apiService.getHistoricalPrice$(undefined).pipe(shareReplay());
|
||||
if (!this.priceObservable$ || (this.priceObservable$ && (now - this.lastPriceHistoryUpdate > 3600 || currency !== this.lastQueriedHistoricalCurrency))) {
|
||||
this.priceObservable$ = this.apiService.getHistoricalPrice$(undefined, currency).pipe(shareReplay());
|
||||
this.lastPriceHistoryUpdate = new Date().getTime() / 1000;
|
||||
this.lastQueriedHistoricalCurrency = currency;
|
||||
}
|
||||
|
||||
return this.priceObservable$.pipe(
|
||||
@ -122,9 +197,15 @@ export class PriceService {
|
||||
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
|
||||
historicalPrice.prices[price.time] = this.stateService.env.ADDITIONAL_CURRENCIES ? {
|
||||
USD: price.USD, EUR: price.EUR, GBP: price.GBP, CAD: price.CAD, CHF: price.CHF, AUD: price.AUD,
|
||||
JPY: price.JPY, BGN: price.BGN, BRL: price.BRL, CNY: price.CNY, CZK: price.CZK, DKK: price.DKK,
|
||||
HKD: price.HKD, HRK: price.HRK, HUF: price.HUF, IDR: price.IDR, ILS: price.ILS, INR: price.INR,
|
||||
ISK: price.ISK, KRW: price.KRW, MXN: price.MXN, MYR: price.MYR, NOK: price.NOK, NZD: price.NZD,
|
||||
PHP: price.PHP, PLN: price.PLN, RON: price.RON, RUB: price.RUB, SEK: price.SEK, SGD: price.SGD,
|
||||
THB: price.THB, TRY: price.TRY, ZAR: price.ZAR
|
||||
} : {
|
||||
USD: price.USD, EUR: price.EUR, GBP: price.GBP, CAD: price.CAD, CHF: price.CHF, AUD: price.AUD, JPY: price.JPY
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -49,6 +49,7 @@ export interface Env {
|
||||
SIGNET_BLOCK_AUDIT_START_HEIGHT: number;
|
||||
HISTORICAL_PRICE: boolean;
|
||||
ACCELERATOR: boolean;
|
||||
ADDITIONAL_CURRENCIES: boolean;
|
||||
GIT_COMMIT_HASH_MEMPOOL_SPACE?: string;
|
||||
PACKAGE_JSON_VERSION_MEMPOOL_SPACE?: string;
|
||||
}
|
||||
@ -82,6 +83,7 @@ const defaultEnv: Env = {
|
||||
'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
||||
'HISTORICAL_PRICE': true,
|
||||
'ACCELERATOR': false,
|
||||
'ADDITIONAL_CURRENCIES': false,
|
||||
};
|
||||
|
||||
@Injectable({
|
||||
|
Loading…
x
Reference in New Issue
Block a user