mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2024-11-19 18:00:17 +01:00
459 lines
14 KiB
JavaScript
459 lines
14 KiB
JavaScript
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,
|
|
HDSegwitBech32Wallet,
|
|
} from './';
|
|
import { LightningCustodianWallet } from './lightning-custodian-wallet';
|
|
import WatchConnectivity from '../WatchConnectivity';
|
|
const encryption = require('../encryption');
|
|
|
|
export class AppStorage {
|
|
static FLAG_ENCRYPTED = 'data_encrypted';
|
|
static LANG = 'lang';
|
|
static EXCHANGE_RATES = 'currency';
|
|
static LNDHUB = 'lndhub';
|
|
static ELECTRUM_HOST = 'electrum_host';
|
|
static ELECTRUM_TCP_PORT = 'electrum_tcp_port';
|
|
static PREFERRED_CURRENCY = 'preferredCurrency';
|
|
static ADVANCED_MODE_ENABLED = 'advancedmodeenabled';
|
|
|
|
constructor() {
|
|
/** {Array.<AbstractWallet>} */
|
|
this.wallets = [];
|
|
this.tx_metadata = {};
|
|
this.cachedPassword = false;
|
|
this.settings = {
|
|
brandingColor: '#ffffff',
|
|
foregroundColor: '#0c2550',
|
|
buttonBackgroundColor: '#ccddf9',
|
|
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',
|
|
failedColor: '#ff0000',
|
|
shadowColor: '#000000',
|
|
inverseForegroundColor: '#ffffff',
|
|
hdborderColor: '#68BBE1',
|
|
hdbackgroundColor: '#ECF9FF',
|
|
lnborderColor: '#F7C056',
|
|
lnbackgroundColor: '#FFFAEF',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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 storageIsEncrypted() {
|
|
let data;
|
|
try {
|
|
data = await this.getItem(AppStorage.FLAG_ENCRYPTED);
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
|
|
return !!data;
|
|
}
|
|
|
|
/**
|
|
* Iterates through all values of `data` trying to
|
|
* decrypt each one, and returns first one successfully decrypted
|
|
*
|
|
* @param data String (serialized array)
|
|
* @param password
|
|
*/
|
|
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;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async encryptStorage(password) {
|
|
// assuming the storage is not yet encrypted
|
|
await this.saveToDisk();
|
|
let data = await this.getItem('data');
|
|
// 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);
|
|
this.cachedPassword = password;
|
|
await this.setItem('data', data);
|
|
await this.setItem(AppStorage.FLAG_ENCRYPTED, '1');
|
|
}
|
|
|
|
/**
|
|
* 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');
|
|
buckets = JSON.parse(buckets);
|
|
buckets.push(encryption.encrypt(JSON.stringify(data), fakePassword));
|
|
this.cachedPassword = fakePassword;
|
|
const bucketsString = JSON.stringify(buckets);
|
|
await this.setItem('data', bucketsString);
|
|
return (await this.getItem('data')) === bucketsString;
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
try {
|
|
let data = await this.getItem('data');
|
|
if (password) {
|
|
data = this.decryptData(data, password);
|
|
if (data) {
|
|
// password is good, cache it
|
|
this.cachedPassword = password;
|
|
}
|
|
}
|
|
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) {
|
|
case SegwitBech32Wallet.type:
|
|
unserializedWallet = SegwitBech32Wallet.fromJson(key);
|
|
break;
|
|
case SegwitP2SHWallet.type:
|
|
unserializedWallet = SegwitP2SHWallet.fromJson(key);
|
|
break;
|
|
case WatchOnlyWallet.type:
|
|
unserializedWallet = WatchOnlyWallet.fromJson(key);
|
|
unserializedWallet.init();
|
|
break;
|
|
case HDLegacyP2PKHWallet.type:
|
|
unserializedWallet = HDLegacyP2PKHWallet.fromJson(key);
|
|
break;
|
|
case HDSegwitP2SHWallet.type:
|
|
unserializedWallet = HDSegwitP2SHWallet.fromJson(key);
|
|
break;
|
|
case HDSegwitBech32Wallet.type:
|
|
unserializedWallet = HDSegwitBech32Wallet.fromJson(key);
|
|
break;
|
|
case HDLegacyBreadwalletWallet.type:
|
|
unserializedWallet = HDLegacyBreadwalletWallet.fromJson(key);
|
|
break;
|
|
case LightningCustodianWallet.type:
|
|
/** @type {LightningCustodianWallet} */
|
|
unserializedWallet = LightningCustodianWallet.fromJson(key);
|
|
let lndhub = false;
|
|
try {
|
|
lndhub = await AsyncStorage.getItem(AppStorage.LNDHUB);
|
|
} 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);
|
|
}
|
|
unserializedWallet.init();
|
|
break;
|
|
case LegacyWallet.type:
|
|
default:
|
|
unserializedWallet = LegacyWallet.fromJson(key);
|
|
break;
|
|
}
|
|
// done
|
|
if (!this.wallets.some(wallet => wallet.getSecret() === unserializedWallet.secret)) {
|
|
this.wallets.push(unserializedWallet);
|
|
this.tx_metadata = data.tx_metadata;
|
|
}
|
|
}
|
|
WatchConnectivity.init();
|
|
WatchConnectivity.shared && (await WatchConnectivity.shared.sendWalletsToWatch());
|
|
return true;
|
|
} else {
|
|
return false; // failed loading data or loading/decryptin data
|
|
}
|
|
} catch (error) {
|
|
console.warn(error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lookup wallet in list by it's secret and
|
|
* remove it from `this.wallets`
|
|
*
|
|
* @param wallet {AbstractWallet}
|
|
*/
|
|
deleteWallet(wallet) {
|
|
let secret = wallet.getSecret();
|
|
let tempWallets = [];
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
async saveToDisk() {
|
|
let walletsToSave = [];
|
|
for (let key of this.wallets) {
|
|
if (typeof key === 'boolean') continue;
|
|
if (key.prepareForSerialization) key.prepareForSerialization();
|
|
walletsToSave.push(JSON.stringify({ ...key, type: key.type }));
|
|
}
|
|
|
|
let data = {
|
|
wallets: walletsToSave,
|
|
tx_metadata: this.tx_metadata,
|
|
};
|
|
|
|
if (this.cachedPassword) {
|
|
// should find the correct bucket, encrypt and then save
|
|
let buckets = await this.getItem('data');
|
|
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
|
|
newData.push(encryption.encrypt(JSON.stringify(data), this.cachedPassword));
|
|
await this.setItem(AppStorage.FLAG_ENCRYPTED, '1');
|
|
}
|
|
}
|
|
data = newData;
|
|
} else {
|
|
await this.setItem(AppStorage.FLAG_ENCRYPTED, ''); // drop the flag
|
|
}
|
|
WatchConnectivity.init();
|
|
WatchConnectivity.shared && WatchConnectivity.shared.sendWalletsToWatch();
|
|
return this.setItem('data', JSON.stringify(data));
|
|
}
|
|
|
|
/**
|
|
* For each wallet, fetches balance from remote endpoint.
|
|
* Use getter for a specific wallet to get actual balance.
|
|
* Returns void.
|
|
* If index is present then fetch only from this specific wallet
|
|
*
|
|
* @return {Promise.<void>}
|
|
*/
|
|
async fetchWalletBalances(index) {
|
|
console.log('fetchWalletBalances for wallet#', index);
|
|
if (index || index === 0) {
|
|
let c = 0;
|
|
for (let wallet of this.wallets) {
|
|
if (c++ === index) {
|
|
await wallet.fetchBalance();
|
|
}
|
|
}
|
|
} else {
|
|
for (let wallet of this.wallets) {
|
|
await wallet.fetchBalance();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetches from remote endpoint all transactions for each wallet.
|
|
* Returns void.
|
|
* To access transactions - get them from each respective wallet.
|
|
* If index is present then fetch only from this specific wallet.
|
|
*
|
|
* @param index {Integer} Index of the wallet in this.wallets array,
|
|
* blank to fetch from all wallets
|
|
* @return {Promise.<void>}
|
|
*/
|
|
async fetchWalletTransactions(index) {
|
|
console.log('fetchWalletTransactions for wallet#', index);
|
|
if (index || index === 0) {
|
|
let c = 0;
|
|
for (let wallet of this.wallets) {
|
|
if (c++ === index) {
|
|
await wallet.fetchTransactions();
|
|
if (wallet.fetchPendingTransactions) {
|
|
await wallet.fetchPendingTransactions();
|
|
}
|
|
if (wallet.fetchUserInvoices) {
|
|
await wallet.fetchUserInvoices();
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
for (let wallet of this.wallets) {
|
|
await wallet.fetchTransactions();
|
|
if (wallet.fetchPendingTransactions) {
|
|
await wallet.fetchPendingTransactions();
|
|
}
|
|
if (wallet.fetchUserInvoices) {
|
|
await wallet.fetchUserInvoices();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @returns {Array.<AbstractWallet>}
|
|
*/
|
|
getWallets() {
|
|
return this.wallets;
|
|
}
|
|
|
|
/**
|
|
* Getter for all transactions in all wallets.
|
|
* But if index is provided - only for wallet with corresponding index
|
|
*
|
|
* @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.
|
|
* @return {Array}
|
|
*/
|
|
getTransactions(index, limit = Infinity) {
|
|
if (index || index === 0) {
|
|
let txs = [];
|
|
let c = 0;
|
|
for (let wallet of this.wallets) {
|
|
if (c++ === index) {
|
|
txs = txs.concat(wallet.getTransactions());
|
|
}
|
|
}
|
|
return txs;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Getter for a sum of all balances of all wallets
|
|
*
|
|
* @return {number}
|
|
*/
|
|
getBalance() {
|
|
let finalBalance = 0;
|
|
for (let wal of this.wallets) {
|
|
finalBalance += wal.balance;
|
|
}
|
|
return finalBalance;
|
|
}
|
|
|
|
/**
|
|
* Simple async sleeper function
|
|
*
|
|
* @param ms {number} Milliseconds to sleep
|
|
* @returns {Promise<Promise<*> | Promise<*>>}
|
|
*/
|
|
async sleep(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
}
|