REF: improved currency exchange module

This commit is contained in:
Overtorment 2021-09-09 20:36:35 +01:00
parent f9576dfc9b
commit a3c984db54
No known key found for this signature in database
GPG key ID: AB15F43F78CCBC06
5 changed files with 109 additions and 79 deletions

71
App.js
View file

@ -38,6 +38,7 @@ import Biometric from './class/biometrics';
import WidgetCommunication from './blue_modules/WidgetCommunication'; import WidgetCommunication from './blue_modules/WidgetCommunication';
import changeNavigationBarColor from 'react-native-navigation-bar-color'; import changeNavigationBarColor from 'react-native-navigation-bar-color';
const A = require('./blue_modules/analytics'); const A = require('./blue_modules/analytics');
const currency = require('./blue_modules/currency');
const eventEmitter = new NativeEventEmitter(NativeModules.EventEmitter); const eventEmitter = new NativeEventEmitter(NativeModules.EventEmitter);
@ -262,43 +263,43 @@ const App = () => {
}; };
const handleAppStateChange = async nextAppState => { const handleAppStateChange = async nextAppState => {
if (wallets.length > 0) { if (wallets.length === 0) return;
if ((appState.current.match(/background/) && nextAppState) === 'active' || nextAppState === undefined) { if ((appState.current.match(/background/) && nextAppState === 'active') || nextAppState === undefined) {
setTimeout(() => A(A.ENUM.APP_UNSUSPENDED), 2000); setTimeout(() => A(A.ENUM.APP_UNSUSPENDED), 2000);
const processed = await processPushNotifications(); currency.updateExchangeRate();
if (processed) return; const processed = await processPushNotifications();
const clipboard = await BlueClipboard.getClipboardContent(); if (processed) return;
const isAddressFromStoredWallet = wallets.some(wallet => { const clipboard = await BlueClipboard.getClipboardContent();
if (wallet.chain === Chain.ONCHAIN) { const isAddressFromStoredWallet = wallets.some(wallet => {
// checking address validity is faster than unwrapping hierarchy only to compare it to garbage if (wallet.chain === Chain.ONCHAIN) {
return wallet.isAddressValid && wallet.isAddressValid(clipboard) && wallet.weOwnAddress(clipboard); // checking address validity is faster than unwrapping hierarchy only to compare it to garbage
} else { return wallet.isAddressValid && wallet.isAddressValid(clipboard) && wallet.weOwnAddress(clipboard);
return wallet.isInvoiceGeneratedByWallet(clipboard) || wallet.weOwnAddress(clipboard); } else {
} return wallet.isInvoiceGeneratedByWallet(clipboard) || wallet.weOwnAddress(clipboard);
});
const isBitcoinAddress = DeeplinkSchemaMatch.isBitcoinAddress(clipboard);
const isLightningInvoice = DeeplinkSchemaMatch.isLightningInvoice(clipboard);
const isLNURL = DeeplinkSchemaMatch.isLnUrl(clipboard);
const isBothBitcoinAndLightning = DeeplinkSchemaMatch.isBothBitcoinAndLightning(clipboard);
if (
!isAddressFromStoredWallet &&
clipboardContent.current !== clipboard &&
(isBitcoinAddress || isLightningInvoice || isLNURL || isBothBitcoinAndLightning)
) {
if (isBitcoinAddress) {
setClipboardContentType(ClipboardContentType.BITCOIN);
} else if (isLightningInvoice || isLNURL) {
setClipboardContentType(ClipboardContentType.LIGHTNING);
} else if (isBothBitcoinAndLightning) {
setClipboardContentType(ClipboardContentType.BITCOIN);
}
setIsClipboardContentModalVisible(true);
} }
clipboardContent.current = clipboard; });
} const isBitcoinAddress = DeeplinkSchemaMatch.isBitcoinAddress(clipboard);
if (nextAppState) { const isLightningInvoice = DeeplinkSchemaMatch.isLightningInvoice(clipboard);
appState.current = nextAppState; const isLNURL = DeeplinkSchemaMatch.isLnUrl(clipboard);
const isBothBitcoinAndLightning = DeeplinkSchemaMatch.isBothBitcoinAndLightning(clipboard);
if (
!isAddressFromStoredWallet &&
clipboardContent.current !== clipboard &&
(isBitcoinAddress || isLightningInvoice || isLNURL || isBothBitcoinAndLightning)
) {
if (isBitcoinAddress) {
setClipboardContentType(ClipboardContentType.BITCOIN);
} else if (isLightningInvoice || isLNURL) {
setClipboardContentType(ClipboardContentType.LIGHTNING);
} else if (isBothBitcoinAndLightning) {
setClipboardContentType(ClipboardContentType.BITCOIN);
}
setIsClipboardContentModalVisible(true);
} }
clipboardContent.current = clipboard;
}
if (nextAppState) {
appState.current = nextAppState;
} }
}; };

View file

@ -69,6 +69,6 @@ const startAndDecrypt = async retry => {
}; };
BlueApp.startAndDecrypt = startAndDecrypt; BlueApp.startAndDecrypt = startAndDecrypt;
currency.startUpdater(); currency.init();
module.exports = BlueApp; module.exports = BlueApp;

View file

@ -5,15 +5,14 @@ import BigNumber from 'bignumber.js';
import { FiatUnit, getFiatRate } from '../models/fiatUnit'; import { FiatUnit, getFiatRate } from '../models/fiatUnit';
import WidgetCommunication from './WidgetCommunication'; import WidgetCommunication from './WidgetCommunication';
const PREFERRED_CURRENCY = 'preferredCurrency'; const PREFERRED_CURRENCY_STORAGE_KEY = 'preferredCurrency';
const EXCHANGE_RATES = 'currency'; const EXCHANGE_RATES_STORAGE_KEY = 'currency';
let preferredFiatCurrency = FiatUnit.USD; let preferredFiatCurrency = FiatUnit.USD;
const exchangeRates = {}; let exchangeRates = {};
let lastTimeUpdateExchangeRateWasCalled = 0;
const STRUCT = { const LAST_UPDATED = 'LAST_UPDATED';
LAST_UPDATED: 'LAST_UPDATED',
};
/** /**
* Saves to storage preferred currency, whole object * Saves to storage preferred currency, whole object
@ -23,7 +22,7 @@ const STRUCT = {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function setPrefferedCurrency(item) { async function setPrefferedCurrency(item) {
await AsyncStorage.setItem(PREFERRED_CURRENCY, JSON.stringify(item)); await AsyncStorage.setItem(PREFERRED_CURRENCY_STORAGE_KEY, JSON.stringify(item));
await DefaultPreference.setName('group.io.bluewallet.bluewallet'); await DefaultPreference.setName('group.io.bluewallet.bluewallet');
await DefaultPreference.set('preferredCurrency', item.endPointKey); await DefaultPreference.set('preferredCurrency', item.endPointKey);
await DefaultPreference.set('preferredCurrencyLocale', item.locale.replace('-', '_')); await DefaultPreference.set('preferredCurrencyLocale', item.locale.replace('-', '_'));
@ -31,21 +30,25 @@ async function setPrefferedCurrency(item) {
} }
async function getPreferredCurrency() { async function getPreferredCurrency() {
const preferredCurrency = await JSON.parse(await AsyncStorage.getItem(PREFERRED_CURRENCY)); const preferredCurrency = await JSON.parse(await AsyncStorage.getItem(PREFERRED_CURRENCY_STORAGE_KEY));
await DefaultPreference.setName('group.io.bluewallet.bluewallet'); await DefaultPreference.setName('group.io.bluewallet.bluewallet');
await DefaultPreference.set('preferredCurrency', preferredCurrency.endPointKey); await DefaultPreference.set('preferredCurrency', preferredCurrency.endPointKey);
await DefaultPreference.set('preferredCurrencyLocale', preferredCurrency.locale.replace('-', '_')); await DefaultPreference.set('preferredCurrencyLocale', preferredCurrency.locale.replace('-', '_'));
return preferredCurrency; return preferredCurrency;
} }
async function updateExchangeRate() { async function _restoreSavedExchangeRatesFromStorage() {
if (+new Date() - exchangeRates[STRUCT.LAST_UPDATED] <= 30 * 60 * 1000) {
// not updating too often
return;
}
try { try {
preferredFiatCurrency = JSON.parse(await AsyncStorage.getItem(PREFERRED_CURRENCY)); exchangeRates = JSON.parse(await AsyncStorage.getItem(EXCHANGE_RATES_STORAGE_KEY));
if (!exchangeRates) exchangeRates = {};
} catch (_) {
exchangeRates = {};
}
}
async function _restoreSavedPreferredFiatCurrencyFromStorage() {
try {
preferredFiatCurrency = JSON.parse(await AsyncStorage.getItem(PREFERRED_CURRENCY_STORAGE_KEY));
if (preferredFiatCurrency === null) { if (preferredFiatCurrency === null) {
throw Error('No Preferred Fiat selected'); throw Error('No Preferred Fiat selected');
} }
@ -57,38 +60,66 @@ async function updateExchangeRate() {
preferredFiatCurrency = FiatUnit.USD; preferredFiatCurrency = FiatUnit.USD;
} }
} }
}
/**
* actual function to reach api and get fresh currency exchange rate. checks LAST_UPDATED time and skips entirely
* if called too soon (30min); saves exchange rate (with LAST_UPDATED info) to storage.
* should be called when app thinks its a good time to refresh exchange rate
*
* @return {Promise<void>}
*/
async function updateExchangeRate() {
if (+new Date() - lastTimeUpdateExchangeRateWasCalled <= 10 * 1000) {
// simple debounce so theres no race conditions
return;
}
lastTimeUpdateExchangeRateWasCalled = +new Date();
if (+new Date() - exchangeRates[LAST_UPDATED] <= 30 * 60 * 1000) {
// not updating too often
return;
}
console.log('updating exchange rate...');
let rate; let rate;
try { try {
rate = await getFiatRate(preferredFiatCurrency.endPointKey); rate = await getFiatRate(preferredFiatCurrency.endPointKey);
} catch (Err) { } catch (Err) {
console.warn(Err.message); console.warn(Err.message);
const lastSavedExchangeRate = JSON.parse(await AsyncStorage.getItem(EXCHANGE_RATES));
exchangeRates['BTC_' + preferredFiatCurrency.endPointKey] = lastSavedExchangeRate['BTC_' + preferredFiatCurrency.endPointKey] * 1;
return; return;
} }
exchangeRates[STRUCT.LAST_UPDATED] = +new Date(); exchangeRates[LAST_UPDATED] = +new Date();
exchangeRates['BTC_' + preferredFiatCurrency.endPointKey] = rate; exchangeRates['BTC_' + preferredFiatCurrency.endPointKey] = rate;
await AsyncStorage.setItem(EXCHANGE_RATES, JSON.stringify(exchangeRates)); await AsyncStorage.setItem(EXCHANGE_RATES_STORAGE_KEY, JSON.stringify(exchangeRates));
await AsyncStorage.setItem(PREFERRED_CURRENCY, JSON.stringify(preferredFiatCurrency));
await setPrefferedCurrency(preferredFiatCurrency);
} }
let interval = false; /**
async function startUpdater() { * this function reads storage and restores current preferred fiat currency & last saved exchange rate, then calls
if (interval) { * updateExchangeRate() to update rates.
clearInterval(interval); * should be called when the app starts and when user changes preferred fiat (with TRUE argument so underlying
exchangeRates[STRUCT.LAST_UPDATED] = 0; * `updateExchangeRate()` would actually update rates via api).
*
* @param clearLastUpdatedTime {boolean} set to TRUE for the underlying
*
* @return {Promise<void>}
*/
async function init(clearLastUpdatedTime = false) {
await _restoreSavedExchangeRatesFromStorage();
await _restoreSavedPreferredFiatCurrencyFromStorage();
if (clearLastUpdatedTime) {
exchangeRates[LAST_UPDATED] = 0;
lastTimeUpdateExchangeRateWasCalled = 0;
} }
interval = setInterval(() => updateExchangeRate(), 2 * 60 * 100);
return updateExchangeRate(); return updateExchangeRate();
} }
function satoshiToLocalCurrency(satoshi) { function satoshiToLocalCurrency(satoshi) {
if (!exchangeRates['BTC_' + preferredFiatCurrency.endPointKey]) { if (!exchangeRates['BTC_' + preferredFiatCurrency.endPointKey]) {
startUpdater(); updateExchangeRate();
return '...'; return '...';
} }
@ -169,8 +200,7 @@ function _setExchangeRate(pair, rate) {
} }
module.exports.updateExchangeRate = updateExchangeRate; module.exports.updateExchangeRate = updateExchangeRate;
module.exports.startUpdater = startUpdater; module.exports.init = init;
module.exports.STRUCT = STRUCT;
module.exports.satoshiToLocalCurrency = satoshiToLocalCurrency; module.exports.satoshiToLocalCurrency = satoshiToLocalCurrency;
module.exports.fiatToBTC = fiatToBTC; module.exports.fiatToBTC = fiatToBTC;
module.exports.satoshiToBTC = satoshiToBTC; module.exports.satoshiToBTC = satoshiToBTC;
@ -181,5 +211,6 @@ module.exports.btcToSatoshi = btcToSatoshi;
module.exports.getCurrencySymbol = getCurrencySymbol; module.exports.getCurrencySymbol = getCurrencySymbol;
module.exports._setPreferredFiatCurrency = _setPreferredFiatCurrency; // export it to mock data in tests module.exports._setPreferredFiatCurrency = _setPreferredFiatCurrency; // export it to mock data in tests
module.exports._setExchangeRate = _setExchangeRate; // export it to mock data in tests module.exports._setExchangeRate = _setExchangeRate; // export it to mock data in tests
module.exports.PREFERRED_CURRENCY = PREFERRED_CURRENCY; module.exports.PREFERRED_CURRENCY = PREFERRED_CURRENCY_STORAGE_KEY;
module.exports.EXCHANGE_RATES = EXCHANGE_RATES; module.exports.EXCHANGE_RATES = EXCHANGE_RATES_STORAGE_KEY;
module.exports.LAST_UPDATED = LAST_UPDATED;

View file

@ -63,7 +63,7 @@ const Currency = () => {
setIsSavingNewPreferredCurrency(true); setIsSavingNewPreferredCurrency(true);
setSelectedCurrency(item); setSelectedCurrency(item);
await currency.setPrefferedCurrency(item); await currency.setPrefferedCurrency(item);
await currency.startUpdater(); await currency.init(true);
setIsSavingNewPreferredCurrency(false); setIsSavingNewPreferredCurrency(false);
setPreferredFiatCurrency(); setPreferredFiatCurrency();
}} }}

View file

@ -3,28 +3,26 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
import { FiatUnit } from '../../models/fiatUnit'; import { FiatUnit } from '../../models/fiatUnit';
jest.useFakeTimers();
describe('currency', () => { describe('currency', () => {
it('fetches exchange rate and saves to AsyncStorage', async () => { it('fetches exchange rate and saves to AsyncStorage', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000;
const currency = require('../../blue_modules/currency'); const currency = require('../../blue_modules/currency');
await currency.startUpdater(); await currency.init();
let cur = await AsyncStorage.getItem(currency.EXCHANGE_RATES); let cur = await AsyncStorage.getItem(currency.EXCHANGE_RATES);
cur = JSON.parse(cur); cur = JSON.parse(cur);
assert.ok(Number.isInteger(cur[currency.STRUCT.LAST_UPDATED])); assert.ok(Number.isInteger(cur[currency.LAST_UPDATED]));
assert.ok(cur[currency.STRUCT.LAST_UPDATED] > 0); assert.ok(cur[currency.LAST_UPDATED] > 0);
assert.ok(cur.BTC_USD > 0); assert.ok(cur.BTC_USD > 0);
// now, setting other currency as default // now, setting other currency as default
await AsyncStorage.setItem(currency.PREFERRED_CURRENCY, JSON.stringify(FiatUnit.JPY)); await AsyncStorage.setItem(currency.PREFERRED_CURRENCY, JSON.stringify(FiatUnit.JPY));
await currency.startUpdater(); await currency.init(true);
cur = JSON.parse(await AsyncStorage.getItem(currency.EXCHANGE_RATES)); cur = JSON.parse(await AsyncStorage.getItem(currency.EXCHANGE_RATES));
assert.ok(cur.BTC_JPY > 0); assert.ok(cur.BTC_JPY > 0);
// now setting with a proper setter // now setting with a proper setter
await currency.setPrefferedCurrency(FiatUnit.EUR); await currency.setPrefferedCurrency(FiatUnit.EUR);
await currency.startUpdater(); await currency.init(true);
const preferred = await currency.getPreferredCurrency(); const preferred = await currency.getPreferredCurrency();
assert.strictEqual(preferred.endPointKey, 'EUR'); assert.strictEqual(preferred.endPointKey, 'EUR');
cur = JSON.parse(await AsyncStorage.getItem(currency.EXCHANGE_RATES)); cur = JSON.parse(await AsyncStorage.getItem(currency.EXCHANGE_RATES));
@ -32,20 +30,20 @@ describe('currency', () => {
// test Yadio rate source // test Yadio rate source
await currency.setPrefferedCurrency(FiatUnit.ARS); await currency.setPrefferedCurrency(FiatUnit.ARS);
await currency.startUpdater(); await currency.init(true);
cur = JSON.parse(await AsyncStorage.getItem(currency.EXCHANGE_RATES)); cur = JSON.parse(await AsyncStorage.getItem(currency.EXCHANGE_RATES));
assert.ok(cur.BTC_ARS > 0); assert.ok(cur.BTC_ARS > 0);
// test BitcoinduLiban rate source // test BitcoinduLiban rate source
// disabled, because it throws "Service Temporarily Unavailable" on circleci // disabled, because it throws "Service Temporarily Unavailable" on circleci
// await currency.setPrefferedCurrency(FiatUnit.LBP); // await currency.setPrefferedCurrency(FiatUnit.LBP);
// await currency.startUpdater(); // await currency.init(true);
// cur = JSON.parse(await AsyncStorage.getItem(currency.EXCHANGE_RATES)); // cur = JSON.parse(await AsyncStorage.getItem(currency.EXCHANGE_RATES));
// assert.ok(cur.BTC_LBP > 0); // assert.ok(cur.BTC_LBP > 0);
// test Exir rate source // test Exir rate source
await currency.setPrefferedCurrency(FiatUnit.IRR); await currency.setPrefferedCurrency(FiatUnit.IRR);
await currency.startUpdater(); await currency.init(true);
cur = JSON.parse(await AsyncStorage.getItem(currency.EXCHANGE_RATES)); cur = JSON.parse(await AsyncStorage.getItem(currency.EXCHANGE_RATES));
assert.ok(cur.BTC_IRR > 0); assert.ok(cur.BTC_IRR > 0);
}); });