mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-03-03 20:07:11 +01:00
REF: improved currency exchange module
This commit is contained in:
parent
f9576dfc9b
commit
a3c984db54
5 changed files with 109 additions and 79 deletions
71
App.js
71
App.js
|
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,6 @@ const startAndDecrypt = async retry => {
|
||||||
};
|
};
|
||||||
|
|
||||||
BlueApp.startAndDecrypt = startAndDecrypt;
|
BlueApp.startAndDecrypt = startAndDecrypt;
|
||||||
currency.startUpdater();
|
currency.init();
|
||||||
|
|
||||||
module.exports = BlueApp;
|
module.exports = BlueApp;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue