BlueWallet/blue_modules/currency.ts

345 lines
11 KiB
TypeScript
Raw Permalink Normal View History

2024-01-28 16:11:08 +01:00
import AsyncStorage from '@react-native-async-storage/async-storage';
2024-05-20 11:54:13 +02:00
import BigNumber from 'bignumber.js';
2024-01-28 16:11:08 +01:00
import DefaultPreference from 'react-native-default-preference';
import * as RNLocalize from 'react-native-localize';
2024-05-20 11:54:13 +02:00
2024-01-28 16:11:08 +01:00
import { FiatUnit, FiatUnitType, getFiatRate } from '../models/fiatUnit';
const PREFERRED_CURRENCY_STORAGE_KEY = 'preferredCurrency';
const PREFERRED_CURRENCY_LOCALE_STORAGE_KEY = 'preferredCurrencyLocale';
2024-01-28 16:11:08 +01:00
const EXCHANGE_RATES_STORAGE_KEY = 'exchangeRates';
const LAST_UPDATED = 'LAST_UPDATED';
export const GROUP_IO_BLUEWALLET = 'group.io.bluewallet.bluewallet';
2024-01-28 16:11:08 +01:00
const BTC_PREFIX = 'BTC_';
export interface CurrencyRate {
LastUpdated: Date | null;
Rate: number | string | null;
}
interface ExchangeRates {
[key: string]: number | boolean | undefined;
LAST_UPDATED_ERROR: boolean;
}
let preferredFiatCurrency: FiatUnitType = FiatUnit.USD;
let exchangeRates: ExchangeRates = { LAST_UPDATED_ERROR: false };
let lastTimeUpdateExchangeRateWasCalled: number = 0;
let skipUpdateExchangeRate: boolean = false;
2024-10-27 19:58:02 +01:00
async function setPreferredCurrency(item: FiatUnitType): Promise<void> {
await AsyncStorage.setItem(PREFERRED_CURRENCY_STORAGE_KEY, JSON.stringify(item));
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
await DefaultPreference.set(PREFERRED_CURRENCY_STORAGE_KEY, item.endPointKey);
await DefaultPreference.set(PREFERRED_CURRENCY_LOCALE_STORAGE_KEY, item.locale.replace('-', '_'));
}
2024-10-27 03:15:53 +01:00
async function updateExchangeRate(): Promise<void> {
if (skipUpdateExchangeRate) return;
if (Date.now() - lastTimeUpdateExchangeRateWasCalled <= 10000) {
// simple debounce so there's no race conditions
return;
}
lastTimeUpdateExchangeRateWasCalled = Date.now();
const lastUpdated = exchangeRates[LAST_UPDATED] as number | undefined;
if (lastUpdated && Date.now() - lastUpdated <= 30 * 60 * 1000) {
// not updating too often
return;
}
console.log('updating exchange rate...');
try {
const rate = await getFiatRate(preferredFiatCurrency.endPointKey);
exchangeRates[LAST_UPDATED] = Date.now();
exchangeRates[BTC_PREFIX + preferredFiatCurrency.endPointKey] = rate;
exchangeRates.LAST_UPDATED_ERROR = false;
2024-10-27 19:58:02 +01:00
try {
const exchangeRatesString = JSON.stringify(exchangeRates);
await AsyncStorage.setItem(EXCHANGE_RATES_STORAGE_KEY, exchangeRatesString);
} catch (error) {
await AsyncStorage.removeItem(EXCHANGE_RATES_STORAGE_KEY);
exchangeRates = { LAST_UPDATED_ERROR: false };
2024-10-27 03:15:53 +01:00
}
2024-10-27 19:58:02 +01:00
} catch (error) {
try {
const ratesString = await AsyncStorage.getItem(EXCHANGE_RATES_STORAGE_KEY);
let rate;
if (ratesString) {
try {
rate = JSON.parse(ratesString);
} catch (parseError) {
await AsyncStorage.removeItem(EXCHANGE_RATES_STORAGE_KEY);
rate = {};
}
} else {
rate = {};
}
rate.LAST_UPDATED_ERROR = true;
exchangeRates.LAST_UPDATED_ERROR = true;
await AsyncStorage.setItem(EXCHANGE_RATES_STORAGE_KEY, JSON.stringify(rate));
} catch (storageError) {}
2024-10-27 03:15:53 +01:00
}
2024-01-28 16:11:08 +01:00
}
async function getPreferredCurrency(): Promise<FiatUnitType> {
const preferredCurrency = await AsyncStorage.getItem(PREFERRED_CURRENCY_STORAGE_KEY);
if (preferredCurrency) {
2024-10-27 19:58:02 +01:00
let parsedPreferredCurrency;
try {
parsedPreferredCurrency = JSON.parse(preferredCurrency);
if (!FiatUnit[parsedPreferredCurrency.endPointKey]) {
throw new Error('Invalid Fiat Unit');
}
preferredFiatCurrency = FiatUnit[parsedPreferredCurrency.endPointKey];
} catch (error) {
await AsyncStorage.removeItem(PREFERRED_CURRENCY_STORAGE_KEY);
const deviceCurrencies = RNLocalize.getCurrencies();
if (deviceCurrencies[0] && FiatUnit[deviceCurrencies[0]]) {
preferredFiatCurrency = FiatUnit[deviceCurrencies[0]];
} else {
preferredFiatCurrency = FiatUnit.USD;
}
}
2024-04-21 04:24:28 +02:00
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
await DefaultPreference.set(PREFERRED_CURRENCY_STORAGE_KEY, preferredFiatCurrency.endPointKey);
await DefaultPreference.set(PREFERRED_CURRENCY_LOCALE_STORAGE_KEY, preferredFiatCurrency.locale.replace('-', '_'));
return preferredFiatCurrency;
}
2024-10-27 19:58:02 +01:00
const deviceCurrencies = RNLocalize.getCurrencies();
if (deviceCurrencies[0] && FiatUnit[deviceCurrencies[0]]) {
preferredFiatCurrency = FiatUnit[deviceCurrencies[0]];
} else {
preferredFiatCurrency = FiatUnit.USD;
}
return preferredFiatCurrency;
2024-01-28 16:11:08 +01:00
}
async function _restoreSavedExchangeRatesFromStorage(): Promise<void> {
try {
const rates = await AsyncStorage.getItem(EXCHANGE_RATES_STORAGE_KEY);
2024-10-27 19:58:02 +01:00
if (rates) {
try {
exchangeRates = JSON.parse(rates);
} catch (error) {
await AsyncStorage.removeItem(EXCHANGE_RATES_STORAGE_KEY);
exchangeRates = { LAST_UPDATED_ERROR: false };
await updateExchangeRate();
}
} else {
exchangeRates = { LAST_UPDATED_ERROR: false };
2024-10-27 03:15:53 +01:00
}
2024-10-27 19:58:02 +01:00
} catch (error) {
exchangeRates = { LAST_UPDATED_ERROR: false };
await updateExchangeRate();
2024-01-28 16:11:08 +01:00
}
}
async function _restoreSavedPreferredFiatCurrencyFromStorage(): Promise<void> {
try {
const storedCurrency = await AsyncStorage.getItem(PREFERRED_CURRENCY_STORAGE_KEY);
if (!storedCurrency) throw new Error('No Preferred Fiat selected');
2024-10-27 19:58:02 +01:00
let parsedCurrency;
try {
parsedCurrency = JSON.parse(storedCurrency);
if (!FiatUnit[parsedCurrency.endPointKey]) {
throw new Error('Invalid Fiat Unit');
}
preferredFiatCurrency = FiatUnit[parsedCurrency.endPointKey];
} catch (error) {
await AsyncStorage.removeItem(PREFERRED_CURRENCY_STORAGE_KEY);
const deviceCurrencies = RNLocalize.getCurrencies();
if (deviceCurrencies[0] && FiatUnit[deviceCurrencies[0]]) {
preferredFiatCurrency = FiatUnit[deviceCurrencies[0]];
} else {
preferredFiatCurrency = FiatUnit.USD;
}
2024-01-28 16:11:08 +01:00
}
2024-10-27 19:58:02 +01:00
} catch (error) {
2024-01-28 16:11:08 +01:00
const deviceCurrencies = RNLocalize.getCurrencies();
2024-10-27 19:58:02 +01:00
if (deviceCurrencies[0] && FiatUnit[deviceCurrencies[0]]) {
preferredFiatCurrency = FiatUnit[deviceCurrencies[0]];
} else {
preferredFiatCurrency = FiatUnit.USD;
}
2024-01-28 16:11:08 +01:00
}
}
async function isRateOutdated(): Promise<boolean> {
try {
2024-10-27 19:58:02 +01:00
const rateString = await AsyncStorage.getItem(EXCHANGE_RATES_STORAGE_KEY);
let rate;
if (rateString) {
try {
rate = JSON.parse(rateString);
} catch (parseError) {
await AsyncStorage.removeItem(EXCHANGE_RATES_STORAGE_KEY);
rate = {};
await updateExchangeRate();
}
} else {
rate = {};
}
2024-01-28 16:11:08 +01:00
return rate.LAST_UPDATED_ERROR || Date.now() - (rate[LAST_UPDATED] || 0) >= 31 * 60 * 1000;
} catch {
return true;
}
}
async function restoreSavedPreferredFiatCurrencyAndExchangeFromStorage(): Promise<void> {
await _restoreSavedExchangeRatesFromStorage();
await _restoreSavedPreferredFiatCurrencyFromStorage();
2024-04-29 15:01:46 +02:00
}
2024-01-28 16:11:08 +01:00
async function initCurrencyDaemon(clearLastUpdatedTime: boolean = false): Promise<void> {
await _restoreSavedExchangeRatesFromStorage();
await _restoreSavedPreferredFiatCurrencyFromStorage();
if (clearLastUpdatedTime) {
exchangeRates[LAST_UPDATED] = 0;
lastTimeUpdateExchangeRateWasCalled = 0;
}
await updateExchangeRate();
}
function satoshiToLocalCurrency(satoshi: number, format: boolean = true): string {
const exchangeRateKey = BTC_PREFIX + preferredFiatCurrency.endPointKey;
const exchangeRate = exchangeRates[exchangeRateKey];
if (typeof exchangeRate !== 'number') {
updateExchangeRate();
return '...';
}
const btcAmount = new BigNumber(satoshi).dividedBy(100000000);
const convertedAmount = btcAmount.multipliedBy(exchangeRate);
let formattedAmount: string;
if (convertedAmount.isGreaterThanOrEqualTo(0.005) || convertedAmount.isLessThanOrEqualTo(-0.005)) {
formattedAmount = convertedAmount.toFixed(2);
} else {
formattedAmount = convertedAmount.toPrecision(2);
}
if (format === false) return formattedAmount;
try {
const formatter = new Intl.NumberFormat(preferredFiatCurrency.locale, {
style: 'currency',
currency: preferredFiatCurrency.endPointKey,
minimumFractionDigits: 2,
maximumFractionDigits: 8,
});
return formatter.format(Number(formattedAmount));
} catch (error) {
console.warn(error);
return formattedAmount;
}
}
function BTCToLocalCurrency(bitcoin: BigNumber.Value): string {
const sat = new BigNumber(bitcoin).multipliedBy(100000000).toNumber();
return satoshiToLocalCurrency(sat);
}
async function mostRecentFetchedRate(): Promise<CurrencyRate> {
2024-10-27 19:58:02 +01:00
try {
const currencyInformationString = await AsyncStorage.getItem(EXCHANGE_RATES_STORAGE_KEY);
let currencyInformation;
if (currencyInformationString) {
try {
currencyInformation = JSON.parse(currencyInformationString);
} catch (parseError) {
await AsyncStorage.removeItem(EXCHANGE_RATES_STORAGE_KEY);
currencyInformation = {};
await updateExchangeRate();
}
} else {
currencyInformation = {};
}
const formatter = new Intl.NumberFormat(preferredFiatCurrency.locale, {
style: 'currency',
currency: preferredFiatCurrency.endPointKey,
});
const rate = currencyInformation[BTC_PREFIX + preferredFiatCurrency.endPointKey];
return {
LastUpdated: currencyInformation[LAST_UPDATED],
Rate: rate ? formatter.format(rate) : '...',
};
} catch {
return {
LastUpdated: null,
Rate: null,
};
}
2024-01-28 16:11:08 +01:00
}
function satoshiToBTC(satoshi: number): string {
return new BigNumber(satoshi).dividedBy(100000000).toString(10);
}
function btcToSatoshi(btc: BigNumber.Value): number {
return new BigNumber(btc).multipliedBy(100000000).toNumber();
}
function fiatToBTC(fiatFloat: number): string {
const exchangeRateKey = BTC_PREFIX + preferredFiatCurrency.endPointKey;
const exchangeRate = exchangeRates[exchangeRateKey];
if (typeof exchangeRate !== 'number') {
throw new Error('Exchange rate not available');
}
const btcAmount = new BigNumber(fiatFloat).dividedBy(exchangeRate);
return btcAmount.toFixed(8);
}
function getCurrencySymbol(): string {
return preferredFiatCurrency.symbol;
}
function _setPreferredFiatCurrency(currency: FiatUnitType): void {
preferredFiatCurrency = currency;
}
function _setExchangeRate(pair: string, rate: number): void {
exchangeRates[pair] = rate;
}
function _setSkipUpdateExchangeRate(): void {
skipUpdateExchangeRate = true;
}
export {
_setExchangeRate,
2024-05-20 11:54:13 +02:00
_setPreferredFiatCurrency,
2024-01-28 16:11:08 +01:00
_setSkipUpdateExchangeRate,
2024-05-20 11:54:13 +02:00
BTCToLocalCurrency,
btcToSatoshi,
2024-01-28 16:11:08 +01:00
EXCHANGE_RATES_STORAGE_KEY,
2024-05-20 11:54:13 +02:00
fiatToBTC,
getCurrencySymbol,
getPreferredCurrency,
initCurrencyDaemon,
isRateOutdated,
2024-01-28 16:11:08 +01:00
LAST_UPDATED,
mostRecentFetchedRate,
2024-05-20 11:54:13 +02:00
PREFERRED_CURRENCY_STORAGE_KEY,
restoreSavedPreferredFiatCurrencyAndExchangeFromStorage,
2024-05-20 11:54:13 +02:00
satoshiToBTC,
satoshiToLocalCurrency,
setPreferredCurrency,
updateExchangeRate,
2024-10-28 03:05:14 +01:00
};