BlueWallet/class/app-storage.js

538 lines
16 KiB
JavaScript
Raw Normal View History

2019-05-02 22:33:03 +02:00
import AsyncStorage from '@react-native-community/async-storage';
import RNSecureKeyStore, { ACCESSIBLE } from 'react-native-secure-key-store';
import {
HDLegacyBreadwalletWallet,
HDSegwitP2SHWallet,
HDLegacyP2PKHWallet,
WatchOnlyWallet,
LegacyWallet,
SegwitP2SHWallet,
SegwitBech32Wallet,
2019-06-01 22:44:39 +02:00
HDSegwitBech32Wallet,
2019-12-27 03:21:07 +01:00
PlaceholderWallet,
LightningCustodianWallet,
} from './';
2019-05-02 22:33:03 +02:00
import WatchConnectivity from '../WatchConnectivity';
import DeviceQuickActions from './quickActions';
2019-05-02 22:33:03 +02:00
const encryption = require('../encryption');
2018-03-20 21:41:07 +01:00
export class AppStorage {
2018-03-31 02:03:58 +02:00
static FLAG_ENCRYPTED = 'data_encrypted';
2018-05-28 21:18:11 +02:00
static LANG = 'lang';
2018-12-31 22:27:03 +01:00
static EXCHANGE_RATES = 'currency';
static LNDHUB = 'lndhub';
2019-07-18 12:22:01 +02:00
static ELECTRUM_HOST = 'electrum_host';
static ELECTRUM_TCP_PORT = 'electrum_tcp_port';
static ELECTRUM_SSL_PORT = 'electrum_ssl_port';
2018-12-31 22:27:03 +01:00
static PREFERRED_CURRENCY = 'preferredCurrency';
2019-05-19 21:49:42 +02:00
static ADVANCED_MODE_ENABLED = 'advancedmodeenabled';
static DELETE_WALLET_AFTER_UNINSTALL = 'deleteWalletAfterUninstall';
2018-03-20 21:41:07 +01:00
constructor() {
/** {Array.<AbstractWallet>} */
this.wallets = [];
this.tx_metadata = {};
2018-03-31 02:03:58 +02:00
this.cachedPassword = false;
2018-03-20 21:41:07 +01:00
this.settings = {
2018-06-25 00:22:46 +02:00
brandingColor: '#ffffff',
foregroundColor: '#0c2550',
buttonBackgroundColor: '#ccddf9',
2018-06-25 00:22:46 +02:00
buttonTextColor: '#0c2550',
buttonAlternativeTextColor: '#2f5fb3',
buttonDisabledBackgroundColor: '#eef0f4',
buttonDisabledTextColor: '#9aa0aa',
inputBorderColor: '#d2d2d2',
inputBackgroundColor: '#f5f5f5',
alternativeTextColor: '#9aa0aa',
alternativeTextColor2: '#0f5cc0',
buttonBlueBackgroundColor: '#ccddf9',
incomingBackgroundColor: '#d2f8d6',
incomingForegroundColor: '#37c0a1',
outgoingBackgroundColor: '#f8d2d2',
outgoingForegroundColor: '#d0021b',
successColor: '#37c0a1',
2019-03-29 15:10:38 +01:00
failedColor: '#ff0000',
shadowColor: '#000000',
inverseForegroundColor: '#ffffff',
hdborderColor: '#68BBE1',
hdbackgroundColor: '#ECF9FF',
lnborderColor: '#F7C056',
lnbackgroundColor: '#FFFAEF',
2018-03-20 21:41:07 +01:00
};
}
/**
* Wrapper for storage call. Secure store works only in RN environment. AsyncStorage is
* used for cli/tests
*
* @param key
* @param value
* @returns {Promise<any>|Promise<any> | Promise<void> | * | Promise | void}
*/
setItem(key, value) {
if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
return RNSecureKeyStore.set(key, value, { accessible: ACCESSIBLE.WHEN_UNLOCKED });
} else {
return AsyncStorage.setItem(key, value);
}
}
/**
* Wrapper for storage call. Secure store works only in RN environment. AsyncStorage is
* used for cli/tests
*
* @param key
* @returns {Promise<any>|*}
*/
getItem(key) {
if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
return RNSecureKeyStore.get(key);
} else {
return AsyncStorage.getItem(key);
}
}
async setResetOnAppUninstallTo(value) {
await this.setItem(AppStorage.DELETE_WALLET_AFTER_UNINSTALL, value ? '1' : '');
try {
await RNSecureKeyStore.setResetOnAppUninstallTo(value);
} catch (Error) {
console.warn(Error);
}
}
2018-03-30 20:31:10 +02:00
async storageIsEncrypted() {
let data;
try {
data = await this.getItem(AppStorage.FLAG_ENCRYPTED);
2018-03-30 20:31:10 +02:00
} catch (error) {
return false;
}
2018-03-31 02:03:58 +02:00
return !!data;
}
2019-08-23 09:04:23 +02:00
async isPasswordInUse(password) {
try {
let data = await this.getItem('data');
data = this.decryptData(data, password);
return !!data;
2019-08-23 09:04:23 +02:00
} catch (_e) {
return false;
}
}
2018-03-31 02:03:58 +02:00
/**
* Iterates through all values of `data` trying to
* decrypt each one, and returns first one successfully decrypted
*
* @param data {string} Serialized array
2018-03-31 02:03:58 +02:00
* @param password
* @returns {boolean|string} Either STRING of storage data (which is stringified JSON) or FALSE, which means failure
2018-03-31 02:03:58 +02:00
*/
decryptData(data, password) {
data = JSON.parse(data);
let decrypted;
for (let value of data) {
try {
decrypted = encryption.decrypt(value, password);
} catch (e) {
console.log(e.message);
}
if (decrypted) {
return decrypted;
}
2018-03-30 20:31:10 +02:00
}
2018-03-31 02:03:58 +02:00
return false;
}
2019-08-23 09:04:23 +02:00
async decryptStorage(password) {
if (password === this.cachedPassword) {
this.cachedPassword = undefined;
await this.setResetOnAppUninstallTo(true);
await this.saveToDisk();
this.wallets = [];
this.tx_metadata = [];
return this.loadFromDisk();
} else {
throw new Error('Wrong password. Please, try again.');
2019-08-23 09:04:23 +02:00
}
}
async isDeleteWalletAfterUninstallEnabled() {
let deleteWalletsAfterUninstall;
try {
deleteWalletsAfterUninstall = await this.getItem(AppStorage.DELETE_WALLET_AFTER_UNINSTALL);
} catch (_e) {
deleteWalletsAfterUninstall = true;
}
return !!deleteWalletsAfterUninstall;
}
2018-03-31 02:03:58 +02:00
async encryptStorage(password) {
// assuming the storage is not yet encrypted
await this.saveToDisk();
let data = await this.getItem('data');
2018-03-31 02:03:58 +02:00
// TODO: refactor ^^^ (should not save & load to fetch data)
let encrypted = encryption.encrypt(data, password);
data = [];
data.push(encrypted); // putting in array as we might have many buckets with storages
data = JSON.stringify(data);
2018-04-01 01:16:42 +02:00
this.cachedPassword = password;
2019-08-22 00:34:03 +02:00
await this.setItem('data', data);
await this.setItem(AppStorage.FLAG_ENCRYPTED, '1');
DeviceQuickActions.clearShortcutItems();
DeviceQuickActions.removeAllWallets();
2018-03-30 20:31:10 +02:00
}
2018-04-01 01:16:42 +02:00
/**
* Cleans up all current application data (wallets, tx metadata etc)
* Encrypts the bucket and saves it storage
*
* @returns {Promise.<boolean>} Success or failure
*/
async createFakeStorage(fakePassword) {
this.wallets = [];
this.tx_metadata = {};
let data = {
wallets: [],
tx_metadata: {},
};
let buckets = await this.getItem('data');
2018-04-01 01:16:42 +02:00
buckets = JSON.parse(buckets);
buckets.push(encryption.encrypt(JSON.stringify(data), fakePassword));
this.cachedPassword = fakePassword;
2019-05-02 22:33:03 +02:00
const bucketsString = JSON.stringify(buckets);
2019-08-22 00:34:03 +02:00
await this.setItem('data', bucketsString);
return (await this.getItem('data')) === bucketsString;
2018-04-01 01:16:42 +02:00
}
2018-03-31 02:03:58 +02:00
/**
* Loads from storage all wallets and
* maps them to `this.wallets`
*
* @param password If present means storage must be decrypted before usage
* @returns {Promise.<boolean>}
*/
async loadFromDisk(password) {
2018-03-20 21:41:07 +01:00
try {
let data = await this.getItem('data');
2018-03-31 02:03:58 +02:00
if (password) {
data = this.decryptData(data, password);
2018-03-31 15:43:08 +02:00
if (data) {
// password is good, cache it
this.cachedPassword = password;
}
2018-03-31 02:03:58 +02:00
}
2018-03-20 21:41:07 +01:00
if (data !== null) {
data = JSON.parse(data);
if (!data.wallets) return false;
let wallets = data.wallets;
for (let key of wallets) {
// deciding which type is wallet and instatiating correct object
let tempObj = JSON.parse(key);
let unserializedWallet;
switch (tempObj.type) {
2019-12-27 03:21:07 +01:00
case PlaceholderWallet.type:
continue;
case SegwitBech32Wallet.type:
2018-03-20 21:41:07 +01:00
unserializedWallet = SegwitBech32Wallet.fromJson(key);
break;
case SegwitP2SHWallet.type:
2018-03-20 21:41:07 +01:00
unserializedWallet = SegwitP2SHWallet.fromJson(key);
break;
case WatchOnlyWallet.type:
2018-07-14 22:15:55 +02:00
unserializedWallet = WatchOnlyWallet.fromJson(key);
2019-05-15 01:19:35 +02:00
unserializedWallet.init();
2018-07-14 22:15:55 +02:00
break;
case HDLegacyP2PKHWallet.type:
unserializedWallet = HDLegacyP2PKHWallet.fromJson(key);
break;
case HDSegwitP2SHWallet.type:
unserializedWallet = HDSegwitP2SHWallet.fromJson(key);
break;
2019-06-01 22:44:39 +02:00
case HDSegwitBech32Wallet.type:
unserializedWallet = HDSegwitBech32Wallet.fromJson(key);
break;
case HDLegacyBreadwalletWallet.type:
unserializedWallet = HDLegacyBreadwalletWallet.fromJson(key);
break;
case LightningCustodianWallet.type:
2018-11-04 22:21:07 +01:00
/** @type {LightningCustodianWallet} */
unserializedWallet = LightningCustodianWallet.fromJson(key);
let lndhub = false;
2018-11-04 22:21:07 +01:00
try {
lndhub = await AsyncStorage.getItem(AppStorage.LNDHUB);
2018-11-04 22:21:07 +01:00
} catch (Error) {
console.warn(Error);
}
if (unserializedWallet.baseURI) {
unserializedWallet.setBaseURI(unserializedWallet.baseURI); // not really necessary, just for the sake of readability
console.log('using saved uri for for ln wallet:', unserializedWallet.baseURI);
} else if (lndhub) {
console.log('using wallet-wide settings ', lndhub, 'for ln wallet');
unserializedWallet.setBaseURI(lndhub);
} else {
console.log('using default', LightningCustodianWallet.defaultBaseUri, 'for ln wallet');
unserializedWallet.setBaseURI(LightningCustodianWallet.defaultBaseUri);
2018-11-04 22:21:07 +01:00
}
unserializedWallet.init();
break;
case LegacyWallet.type:
2018-03-20 21:41:07 +01:00
default:
unserializedWallet = LegacyWallet.fromJson(key);
break;
}
// done
2018-12-29 18:41:38 +01:00
if (!this.wallets.some(wallet => wallet.getSecret() === unserializedWallet.secret)) {
this.wallets.push(unserializedWallet);
this.tx_metadata = data.tx_metadata;
}
2018-03-20 21:41:07 +01:00
}
WatchConnectivity.shared.wallets = this.wallets;
WatchConnectivity.shared.tx_metadata = this.tx_metadata;
WatchConnectivity.shared.fetchTransactionsFunction = async () => {
await this.fetchWalletTransactions();
await this.saveToDisk();
};
await WatchConnectivity.shared.sendWalletsToWatch();
2020-04-07 19:27:01 +02:00
const isStorageEncrypted = await this.storageIsEncrypted();
if (isStorageEncrypted) {
DeviceQuickActions.clearShortcutItems();
DeviceQuickActions.removeAllWallets();
} else {
DeviceQuickActions.setWallets(this.wallets);
DeviceQuickActions.setQuickActions();
}
2018-03-31 02:03:58 +02:00
return true;
} else {
return false; // failed loading data or loading/decryptin data
2018-03-20 21:41:07 +01:00
}
} catch (error) {
2019-05-15 01:19:35 +02:00
console.warn(error.message);
2018-03-20 21:41:07 +01:00
return false;
}
}
/**
2018-07-02 11:48:40 +02:00
* Lookup wallet in list by it's secret and
* remove it from `this.wallets`
2018-03-20 21:41:07 +01:00
*
* @param wallet {AbstractWallet}
*/
deleteWallet(wallet) {
let secret = wallet.getSecret();
let tempWallets = [];
2018-03-20 21:41:07 +01:00
for (let value of this.wallets) {
if (value.getSecret() === secret) {
// the one we should delete
// nop
} else {
// the one we must keep
tempWallets.push(value);
}
}
this.wallets = tempWallets;
}
2018-03-31 02:03:58 +02:00
/**
* Serializes and saves to storage object data.
* If cached password is saved - finds the correct bucket
* to save to, encrypts and then saves.
*
* @returns {Promise} Result of storage save
2018-03-31 02:03:58 +02:00
*/
async saveToDisk() {
2018-03-20 21:41:07 +01:00
let walletsToSave = [];
for (let key of this.wallets) {
2019-12-27 03:21:07 +01:00
if (typeof key === 'boolean' || key.type === PlaceholderWallet.type) continue;
if (key.prepareForSerialization) key.prepareForSerialization();
walletsToSave.push(JSON.stringify({ ...key, type: key.type }));
2018-03-20 21:41:07 +01:00
}
let data = {
wallets: walletsToSave,
tx_metadata: this.tx_metadata,
};
2018-03-31 02:03:58 +02:00
if (this.cachedPassword) {
// should find the correct bucket, encrypt and then save
let buckets = await this.getItem('data');
2018-03-31 02:03:58 +02:00
buckets = JSON.parse(buckets);
let newData = [];
for (let bucket of buckets) {
let decrypted = encryption.decrypt(bucket, this.cachedPassword);
if (!decrypted) {
// no luck decrypting, its not our bucket
newData.push(bucket);
} else {
// decrypted ok, this is our bucket
// we serialize our object's data, encrypt it, and add it to buckets
2018-07-07 15:04:32 +02:00
newData.push(encryption.encrypt(JSON.stringify(data), this.cachedPassword));
2019-08-22 00:34:03 +02:00
await this.setItem(AppStorage.FLAG_ENCRYPTED, '1');
2018-03-31 02:03:58 +02:00
}
}
data = newData;
2018-03-31 15:43:08 +02:00
} else {
2019-08-22 00:34:03 +02:00
await this.setItem(AppStorage.FLAG_ENCRYPTED, ''); // drop the flag
2018-03-31 02:03:58 +02:00
}
WatchConnectivity.shared.wallets = this.wallets;
WatchConnectivity.shared.tx_metadata = this.tx_metadata;
WatchConnectivity.shared.sendWalletsToWatch();
DeviceQuickActions.setWallets(this.wallets);
DeviceQuickActions.setQuickActions();
2019-08-22 00:34:03 +02:00
return this.setItem('data', JSON.stringify(data));
2018-03-20 21:41:07 +01:00
}
2018-06-17 12:46:19 +02:00
/**
* For each wallet, fetches balance from remote endpoint.
* Use getter for a specific wallet to get actual balance.
* Returns void.
2018-06-28 03:43:28 +02:00
* If index is present then fetch only from this specific wallet
2018-06-17 12:46:19 +02:00
*
* @return {Promise.<void>}
*/
2018-06-28 03:43:28 +02:00
async fetchWalletBalances(index) {
2019-02-17 02:22:14 +01:00
console.log('fetchWalletBalances for wallet#', index);
2018-06-28 03:43:28 +02:00
if (index || index === 0) {
let c = 0;
2019-12-27 03:21:07 +01:00
for (let wallet of this.wallets.filter(wallet => wallet.type !== PlaceholderWallet.type)) {
2018-06-28 03:43:28 +02:00
if (c++ === index) {
await wallet.fetchBalance();
}
}
} else {
2019-12-27 03:21:07 +01:00
for (let wallet of this.wallets.filter(wallet => wallet.type !== PlaceholderWallet.type)) {
2018-06-28 03:43:28 +02:00
await wallet.fetchBalance();
}
2018-03-20 21:41:07 +01:00
}
}
2018-06-17 12:46:19 +02:00
/**
* Fetches from remote endpoint all transactions for each wallet.
* Returns void.
* To access transactions - get them from each respective wallet.
2018-06-28 03:43:28 +02:00
* If index is present then fetch only from this specific wallet.
2018-06-17 12:46:19 +02:00
*
2018-06-25 00:22:46 +02:00
* @param index {Integer} Index of the wallet in this.wallets array,
* blank to fetch from all wallets
2018-06-17 12:46:19 +02:00
* @return {Promise.<void>}
*/
2018-06-25 00:22:46 +02:00
async fetchWalletTransactions(index) {
2019-02-17 02:22:14 +01:00
console.log('fetchWalletTransactions for wallet#', index);
2018-06-25 00:22:46 +02:00
if (index || index === 0) {
let c = 0;
2019-12-27 03:21:07 +01:00
for (let wallet of this.wallets.filter(wallet => wallet.type !== PlaceholderWallet.type)) {
2018-06-25 00:22:46 +02:00
if (c++ === index) {
await wallet.fetchTransactions();
if (wallet.fetchPendingTransactions) {
await wallet.fetchPendingTransactions();
}
if (wallet.fetchUserInvoices) {
await wallet.fetchUserInvoices();
}
2018-06-25 00:22:46 +02:00
}
}
} else {
for (let wallet of this.wallets) {
await wallet.fetchTransactions();
if (wallet.fetchPendingTransactions) {
await wallet.fetchPendingTransactions();
}
if (wallet.fetchUserInvoices) {
await wallet.fetchUserInvoices();
}
2018-06-25 00:22:46 +02:00
}
2018-03-20 21:41:07 +01:00
}
}
/**
*
* @returns {Array.<AbstractWallet>}
*/
getWallets() {
return this.wallets;
}
2018-06-17 12:46:19 +02:00
/**
2018-06-25 00:22:46 +02:00
* Getter for all transactions in all wallets.
* But if index is provided - only for wallet with corresponding index
2018-06-17 12:46:19 +02:00
*
* @param index {Integer|null} Wallet index in this.wallets. Empty (or null) for all wallets.
* @param limit {Integer} How many txs return, starting from the earliest. Default: all of them.
2018-06-17 12:46:19 +02:00
* @return {Array}
*/
getTransactions(index, limit = Infinity) {
2018-06-25 00:22:46 +02:00
if (index || index === 0) {
let txs = [];
let c = 0;
for (let wallet of this.wallets) {
if (c++ === index) {
txs = txs.concat(wallet.getTransactions());
2018-06-25 00:22:46 +02:00
}
}
return txs;
}
2018-03-20 21:41:07 +01:00
let txs = [];
for (let wallet of this.wallets) {
let walletTransactions = wallet.getTransactions();
for (let t of walletTransactions) {
t.walletPreferredBalanceUnit = wallet.getPreferredBalanceUnit();
}
txs = txs.concat(walletTransactions);
2018-03-20 21:41:07 +01:00
}
for (let t of txs) {
t.sort_ts = +new Date(t.received);
}
return txs
.sort(function(a, b) {
return b.sort_ts - a.sort_ts;
})
.slice(0, limit);
2018-03-20 21:41:07 +01:00
}
2018-06-17 12:46:19 +02:00
/**
* Getter for a sum of all balances of all wallets
*
* @return {number}
*/
2018-03-20 21:41:07 +01:00
getBalance() {
let finalBalance = 0;
for (let wal of this.wallets) {
2019-11-29 00:16:04 +01:00
finalBalance += wal.getBalance();
2018-03-20 21:41:07 +01:00
}
return finalBalance;
}
async isAdancedModeEnabled() {
try {
return !!(await this.getItem(AppStorage.ADVANCED_MODE_ENABLED));
} catch (_) {}
return false;
}
async setIsAdancedModeEnabled(value) {
await this.setItem(AppStorage.ADVANCED_MODE_ENABLED, value ? '1' : '');
}
/**
* Simple async sleeper function
*
* @param ms {number} Milliseconds to sleep
* @returns {Promise<Promise<*> | Promise<*>>}
*/
async sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}