mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-01-18 13:26:33 +01:00
REF: app-storage class
This commit is contained in:
parent
ae7d5187a7
commit
bcaec353cb
936
BlueApp.ts
936
BlueApp.ts
@ -1,943 +1,9 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import createHash from 'create-hash';
|
||||
import { Platform } from 'react-native';
|
||||
import DefaultPreference from 'react-native-default-preference';
|
||||
import * as Keychain from 'react-native-keychain';
|
||||
import RNSecureKeyStore, { ACCESSIBLE } from 'react-native-secure-key-store';
|
||||
import Realm from 'realm';
|
||||
import { initCurrencyDaemon } from './blue_modules/currency';
|
||||
import * as encryption from './blue_modules/encryption';
|
||||
import {
|
||||
HDAezeedWallet,
|
||||
HDLegacyBreadwalletWallet,
|
||||
HDLegacyElectrumSeedP2PKHWallet,
|
||||
HDLegacyP2PKHWallet,
|
||||
HDSegwitBech32Wallet,
|
||||
HDSegwitElectrumSeedP2WPKHWallet,
|
||||
HDSegwitP2SHWallet,
|
||||
LegacyWallet,
|
||||
LightningCustodianWallet,
|
||||
LightningLdkWallet,
|
||||
MultisigHDWallet,
|
||||
SLIP39LegacyP2PKHWallet,
|
||||
SLIP39SegwitBech32Wallet,
|
||||
SLIP39SegwitP2SHWallet,
|
||||
SegwitBech32Wallet,
|
||||
SegwitP2SHWallet,
|
||||
WatchOnlyWallet,
|
||||
} from './class/';
|
||||
import Biometric from './class/biometrics';
|
||||
import { randomBytes } from './class/rng';
|
||||
import { TWallet, Transaction } from './class/wallets/types';
|
||||
import presentAlert from './components/Alert';
|
||||
import prompt from './helpers/prompt';
|
||||
import RNFS from 'react-native-fs';
|
||||
import loc from './loc';
|
||||
|
||||
let usedBucketNum: boolean | number = false;
|
||||
let savingInProgress = 0; // its both a flag and a counter of attempts to write to disk
|
||||
|
||||
export type TTXMetadata = {
|
||||
[txid: string]: {
|
||||
memo?: string;
|
||||
txhex?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type TRealmTransaction = {
|
||||
internal: boolean;
|
||||
index: number;
|
||||
tx: string;
|
||||
};
|
||||
|
||||
const isReactNative = typeof navigator !== 'undefined' && navigator?.product === 'ReactNative';
|
||||
|
||||
export class AppStorage {
|
||||
static FLAG_ENCRYPTED = 'data_encrypted';
|
||||
static LNDHUB = 'lndhub';
|
||||
static ADVANCED_MODE_ENABLED = 'advancedmodeenabled';
|
||||
static DO_NOT_TRACK = 'donottrack';
|
||||
static HANDOFF_STORAGE_KEY = 'HandOff';
|
||||
|
||||
static keys2migrate = [AppStorage.HANDOFF_STORAGE_KEY, AppStorage.DO_NOT_TRACK, AppStorage.ADVANCED_MODE_ENABLED];
|
||||
|
||||
public cachedPassword?: false | string;
|
||||
public tx_metadata: TTXMetadata;
|
||||
public wallets: TWallet[];
|
||||
|
||||
constructor() {
|
||||
this.wallets = [];
|
||||
this.tx_metadata = {};
|
||||
this.cachedPassword = false;
|
||||
}
|
||||
|
||||
async migrateKeys() {
|
||||
// do not migrate keys if we are not in RN env
|
||||
if (!isReactNative) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key of AppStorage.keys2migrate) {
|
||||
try {
|
||||
const value = await RNSecureKeyStore.get(key);
|
||||
if (value) {
|
||||
await AsyncStorage.setItem(key, value);
|
||||
await RNSecureKeyStore.remove(key);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for storage call. Secure store works only in RN environment. AsyncStorage is
|
||||
* used for cli/tests
|
||||
*/
|
||||
setItem = (key: string, value: any): Promise<any> => {
|
||||
if (isReactNative) {
|
||||
return RNSecureKeyStore.set(key, value, { accessible: ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY });
|
||||
} else {
|
||||
return AsyncStorage.setItem(key, value);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper for storage call. Secure store works only in RN environment. AsyncStorage is
|
||||
* used for cli/tests
|
||||
*/
|
||||
getItem = (key: string): Promise<any> => {
|
||||
if (isReactNative) {
|
||||
return RNSecureKeyStore.get(key);
|
||||
} else {
|
||||
return AsyncStorage.getItem(key);
|
||||
}
|
||||
};
|
||||
|
||||
getItemWithFallbackToRealm = async (key: string): Promise<any | null> => {
|
||||
let value;
|
||||
try {
|
||||
return await this.getItem(key);
|
||||
} catch (error: any) {
|
||||
console.warn('error reading', key, error.message);
|
||||
console.warn('fallback to realm');
|
||||
const realmKeyValue = await this.openRealmKeyValue();
|
||||
const obj = realmKeyValue.objectForPrimaryKey('KeyValue', key); // search for a realm object with a primary key
|
||||
value = obj?.value;
|
||||
realmKeyValue.close();
|
||||
if (value) {
|
||||
// @ts-ignore value.length
|
||||
console.warn('successfully recovered', value.length, 'bytes from realm for key', key);
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
storageIsEncrypted = async (): Promise<boolean> => {
|
||||
let data;
|
||||
try {
|
||||
data = await this.getItemWithFallbackToRealm(AppStorage.FLAG_ENCRYPTED);
|
||||
} catch (error: any) {
|
||||
console.warn('error reading `' + AppStorage.FLAG_ENCRYPTED + '` key:', error.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(data);
|
||||
};
|
||||
|
||||
isPasswordInUse = async (password: string) => {
|
||||
try {
|
||||
let data = await this.getItem('data');
|
||||
data = this.decryptData(data, password);
|
||||
return Boolean(data);
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Iterates through all values of `data` trying to
|
||||
* decrypt each one, and returns first one successfully decrypted
|
||||
*/
|
||||
decryptData(data: string, password: string): boolean | string {
|
||||
data = JSON.parse(data);
|
||||
let decrypted;
|
||||
let num = 0;
|
||||
for (const value of data) {
|
||||
decrypted = encryption.decrypt(value, password);
|
||||
|
||||
if (decrypted) {
|
||||
usedBucketNum = num;
|
||||
return decrypted;
|
||||
}
|
||||
num++;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
decryptStorage = async (password: string): Promise<boolean> => {
|
||||
if (password === this.cachedPassword) {
|
||||
this.cachedPassword = undefined;
|
||||
await this.saveToDisk();
|
||||
this.wallets = [];
|
||||
this.tx_metadata = {};
|
||||
return this.loadFromDisk();
|
||||
} else {
|
||||
throw new Error('Incorrect password. Please, try again.');
|
||||
}
|
||||
};
|
||||
|
||||
encryptStorage = async (password: string): Promise<void> => {
|
||||
// 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)
|
||||
|
||||
const 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
|
||||
*/
|
||||
createFakeStorage = async (fakePassword: string): Promise<boolean> => {
|
||||
usedBucketNum = false; // resetting currently used bucket so we wont overwrite it
|
||||
this.wallets = [];
|
||||
this.tx_metadata = {};
|
||||
|
||||
const 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;
|
||||
};
|
||||
|
||||
hashIt = (s: string): string => {
|
||||
return createHash('sha256').update(s).digest().toString('hex');
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns instace of the Realm database, which is encrypted either by cached user's password OR default password.
|
||||
* Database file is deterministically derived from encryption key.
|
||||
*/
|
||||
async getRealm() {
|
||||
const cacheFolderPath = RNFS.CachesDirectoryPath; // Path to cache folder
|
||||
const password = this.hashIt(this.cachedPassword || 'fyegjitkyf[eqjnc.lf');
|
||||
const buf = Buffer.from(this.hashIt(password) + this.hashIt(password), 'hex');
|
||||
const encryptionKey = Int8Array.from(buf);
|
||||
const fileName = this.hashIt(this.hashIt(password)) + '-wallettransactions.realm';
|
||||
const path = `${cacheFolderPath}/${fileName}`; // Use cache folder path
|
||||
|
||||
const schema = [
|
||||
{
|
||||
name: 'WalletTransactions',
|
||||
properties: {
|
||||
walletid: { type: 'string', indexed: true },
|
||||
internal: 'bool?', // true - internal, false - external
|
||||
index: 'int?',
|
||||
tx: 'string', // stringified json
|
||||
},
|
||||
},
|
||||
];
|
||||
// @ts-ignore schema doesn't match Realm's schema type
|
||||
return Realm.open({
|
||||
// @ts-ignore schema doesn't match Realm's schema type
|
||||
schema,
|
||||
path,
|
||||
encryptionKey,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns instace of the Realm database, which is encrypted by device unique id
|
||||
* Database file is static.
|
||||
*
|
||||
* @returns {Promise<Realm>}
|
||||
*/
|
||||
async openRealmKeyValue(): Promise<Realm> {
|
||||
const cacheFolderPath = RNFS.CachesDirectoryPath; // Path to cache folder
|
||||
const service = 'realm_encryption_key';
|
||||
let password;
|
||||
const credentials = await Keychain.getGenericPassword({ service });
|
||||
if (credentials) {
|
||||
password = credentials.password;
|
||||
} else {
|
||||
const buf = await randomBytes(64);
|
||||
password = buf.toString('hex');
|
||||
await Keychain.setGenericPassword(service, password, { service });
|
||||
}
|
||||
|
||||
const buf = Buffer.from(password, 'hex');
|
||||
const encryptionKey = Int8Array.from(buf);
|
||||
const path = `${cacheFolderPath}/keyvalue.realm`; // Use cache folder path
|
||||
|
||||
const schema = [
|
||||
{
|
||||
name: 'KeyValue',
|
||||
primaryKey: 'key',
|
||||
properties: {
|
||||
key: { type: 'string', indexed: true },
|
||||
value: 'string', // stringified json, or whatever
|
||||
},
|
||||
},
|
||||
];
|
||||
// @ts-ignore schema doesn't match Realm's schema type
|
||||
return Realm.open({
|
||||
// @ts-ignore schema doesn't match Realm's schema type
|
||||
schema,
|
||||
path,
|
||||
encryptionKey,
|
||||
});
|
||||
}
|
||||
|
||||
saveToRealmKeyValue(realmkeyValue: Realm, key: string, value: any) {
|
||||
realmkeyValue.write(() => {
|
||||
realmkeyValue.create(
|
||||
'KeyValue',
|
||||
{
|
||||
key,
|
||||
value,
|
||||
},
|
||||
Realm.UpdateMode.Modified,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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?: string): Promise<boolean> {
|
||||
// Wrap inside a try so if anything goes wrong it wont block loadFromDisk from continuing
|
||||
try {
|
||||
await this.moveRealmFilesToCacheDirectory();
|
||||
} catch (error: any) {
|
||||
console.warn('moveRealmFilesToCacheDirectory error:', error.message);
|
||||
}
|
||||
let data = await this.getItemWithFallbackToRealm('data');
|
||||
if (password) {
|
||||
data = this.decryptData(data, password);
|
||||
if (data) {
|
||||
// password is good, cache it
|
||||
this.cachedPassword = password;
|
||||
}
|
||||
}
|
||||
if (data !== null) {
|
||||
let realm;
|
||||
try {
|
||||
realm = await this.getRealm();
|
||||
} catch (error: any) {
|
||||
presentAlert({ message: error.message });
|
||||
}
|
||||
data = JSON.parse(data);
|
||||
if (!data.wallets) return false;
|
||||
const wallets = data.wallets;
|
||||
for (const key of wallets) {
|
||||
// deciding which type is wallet and instatiating correct object
|
||||
const tempObj = JSON.parse(key);
|
||||
let unserializedWallet: TWallet;
|
||||
switch (tempObj.type) {
|
||||
case SegwitBech32Wallet.type:
|
||||
unserializedWallet = SegwitBech32Wallet.fromJson(key) as unknown as SegwitBech32Wallet;
|
||||
break;
|
||||
case SegwitP2SHWallet.type:
|
||||
unserializedWallet = SegwitP2SHWallet.fromJson(key) as unknown as SegwitP2SHWallet;
|
||||
break;
|
||||
case WatchOnlyWallet.type:
|
||||
unserializedWallet = WatchOnlyWallet.fromJson(key) as unknown as WatchOnlyWallet;
|
||||
unserializedWallet.init();
|
||||
if (unserializedWallet.isHd() && !unserializedWallet.isXpubValid()) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case HDLegacyP2PKHWallet.type:
|
||||
unserializedWallet = HDLegacyP2PKHWallet.fromJson(key) as unknown as HDLegacyP2PKHWallet;
|
||||
break;
|
||||
case HDSegwitP2SHWallet.type:
|
||||
unserializedWallet = HDSegwitP2SHWallet.fromJson(key) as unknown as HDSegwitP2SHWallet;
|
||||
break;
|
||||
case HDSegwitBech32Wallet.type:
|
||||
unserializedWallet = HDSegwitBech32Wallet.fromJson(key) as unknown as HDSegwitBech32Wallet;
|
||||
break;
|
||||
case HDLegacyBreadwalletWallet.type:
|
||||
unserializedWallet = HDLegacyBreadwalletWallet.fromJson(key) as unknown as HDLegacyBreadwalletWallet;
|
||||
break;
|
||||
case HDLegacyElectrumSeedP2PKHWallet.type:
|
||||
unserializedWallet = HDLegacyElectrumSeedP2PKHWallet.fromJson(key) as unknown as HDLegacyElectrumSeedP2PKHWallet;
|
||||
break;
|
||||
case HDSegwitElectrumSeedP2WPKHWallet.type:
|
||||
unserializedWallet = HDSegwitElectrumSeedP2WPKHWallet.fromJson(key) as unknown as HDSegwitElectrumSeedP2WPKHWallet;
|
||||
break;
|
||||
case MultisigHDWallet.type:
|
||||
unserializedWallet = MultisigHDWallet.fromJson(key) as unknown as MultisigHDWallet;
|
||||
break;
|
||||
case HDAezeedWallet.type:
|
||||
unserializedWallet = HDAezeedWallet.fromJson(key) as unknown as HDAezeedWallet;
|
||||
// migrate password to this.passphrase field
|
||||
// remove this code somewhere in year 2022
|
||||
if (unserializedWallet.secret.includes(':')) {
|
||||
const [mnemonic, passphrase] = unserializedWallet.secret.split(':');
|
||||
unserializedWallet.secret = mnemonic;
|
||||
unserializedWallet.passphrase = passphrase;
|
||||
}
|
||||
|
||||
break;
|
||||
case LightningLdkWallet.type:
|
||||
unserializedWallet = LightningLdkWallet.fromJson(key) as unknown as LightningLdkWallet;
|
||||
break;
|
||||
case SLIP39SegwitP2SHWallet.type:
|
||||
unserializedWallet = SLIP39SegwitP2SHWallet.fromJson(key) as unknown as SLIP39SegwitP2SHWallet;
|
||||
break;
|
||||
case SLIP39LegacyP2PKHWallet.type:
|
||||
unserializedWallet = SLIP39LegacyP2PKHWallet.fromJson(key) as unknown as SLIP39LegacyP2PKHWallet;
|
||||
break;
|
||||
case SLIP39SegwitBech32Wallet.type:
|
||||
unserializedWallet = SLIP39SegwitBech32Wallet.fromJson(key) as unknown as SLIP39SegwitBech32Wallet;
|
||||
break;
|
||||
case LightningCustodianWallet.type: {
|
||||
unserializedWallet = LightningCustodianWallet.fromJson(key) as unknown as LightningCustodianWallet;
|
||||
let lndhub: false | any = 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('wallet does not have a baseURI. Continuing init...');
|
||||
}
|
||||
unserializedWallet.init();
|
||||
break;
|
||||
}
|
||||
case LegacyWallet.type:
|
||||
default:
|
||||
unserializedWallet = LegacyWallet.fromJson(key) as unknown as LegacyWallet;
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
if (realm) this.inflateWalletFromRealm(realm, unserializedWallet);
|
||||
} catch (error: any) {
|
||||
presentAlert({ message: error.message });
|
||||
}
|
||||
|
||||
// done
|
||||
const ID = unserializedWallet.getID();
|
||||
if (!this.wallets.some(wallet => wallet.getID() === ID)) {
|
||||
this.wallets.push(unserializedWallet);
|
||||
this.tx_metadata = data.tx_metadata;
|
||||
}
|
||||
}
|
||||
if (realm) realm.close();
|
||||
return true;
|
||||
} else {
|
||||
return false; // failed loading data or loading/decryptin data
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup wallet in list by it's secret and
|
||||
* remove it from `this.wallets`
|
||||
*
|
||||
* @param wallet {AbstractWallet}
|
||||
*/
|
||||
deleteWallet = (wallet: TWallet): void => {
|
||||
const ID = wallet.getID();
|
||||
const tempWallets = [];
|
||||
|
||||
if (wallet.type === LightningLdkWallet.type) {
|
||||
const ldkwallet = wallet;
|
||||
ldkwallet.stop().then(ldkwallet.purgeLocalStorage).catch(alert);
|
||||
}
|
||||
|
||||
for (const value of this.wallets) {
|
||||
if (value.getID() === ID) {
|
||||
// the one we should delete
|
||||
// nop
|
||||
} else {
|
||||
// the one we must keep
|
||||
tempWallets.push(value);
|
||||
}
|
||||
}
|
||||
this.wallets = tempWallets;
|
||||
};
|
||||
|
||||
inflateWalletFromRealm(realm: Realm, walletToInflate: TWallet) {
|
||||
const transactions = realm.objects('WalletTransactions');
|
||||
const transactionsForWallet = transactions.filtered(`walletid = "${walletToInflate.getID()}"`) as unknown as TRealmTransaction[];
|
||||
for (const tx of transactionsForWallet) {
|
||||
if (tx.internal === false) {
|
||||
if ('_hdWalletInstance' in walletToInflate && walletToInflate._hdWalletInstance) {
|
||||
const hd = walletToInflate._hdWalletInstance;
|
||||
hd._txs_by_external_index[tx.index] = hd._txs_by_external_index[tx.index] || [];
|
||||
const transaction = JSON.parse(tx.tx);
|
||||
hd._txs_by_external_index[tx.index].push(transaction);
|
||||
} else {
|
||||
walletToInflate._txs_by_external_index[tx.index] = walletToInflate._txs_by_external_index[tx.index] || [];
|
||||
const transaction = JSON.parse(tx.tx);
|
||||
(walletToInflate._txs_by_external_index[tx.index] as Transaction[]).push(transaction);
|
||||
}
|
||||
} else if (tx.internal === true) {
|
||||
if ('_hdWalletInstance' in walletToInflate && walletToInflate._hdWalletInstance) {
|
||||
const hd = walletToInflate._hdWalletInstance;
|
||||
hd._txs_by_internal_index[tx.index] = hd._txs_by_internal_index[tx.index] || [];
|
||||
const transaction = JSON.parse(tx.tx);
|
||||
hd._txs_by_internal_index[tx.index].push(transaction);
|
||||
} else {
|
||||
walletToInflate._txs_by_internal_index[tx.index] = walletToInflate._txs_by_internal_index[tx.index] || [];
|
||||
const transaction = JSON.parse(tx.tx);
|
||||
(walletToInflate._txs_by_internal_index[tx.index] as Transaction[]).push(transaction);
|
||||
}
|
||||
} else {
|
||||
if (!Array.isArray(walletToInflate._txs_by_external_index)) walletToInflate._txs_by_external_index = [];
|
||||
walletToInflate._txs_by_external_index = walletToInflate._txs_by_external_index || [];
|
||||
const transaction = JSON.parse(tx.tx);
|
||||
(walletToInflate._txs_by_external_index as Transaction[]).push(transaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
offloadWalletToRealm(realm: Realm, wallet: TWallet): void {
|
||||
const id = wallet.getID();
|
||||
const walletToSave = ('_hdWalletInstance' in wallet && wallet._hdWalletInstance) || wallet;
|
||||
|
||||
if (Array.isArray(walletToSave._txs_by_external_index)) {
|
||||
// if this var is an array that means its a single-address wallet class, and this var is a flat array
|
||||
// with transactions
|
||||
realm.write(() => {
|
||||
// cleanup all existing transactions for the wallet first
|
||||
const walletTransactionsToDelete = realm.objects('WalletTransactions').filtered(`walletid = '${id}'`);
|
||||
realm.delete(walletTransactionsToDelete);
|
||||
|
||||
// @ts-ignore walletToSave._txs_by_external_index is array
|
||||
for (const tx of walletToSave._txs_by_external_index) {
|
||||
realm.create(
|
||||
'WalletTransactions',
|
||||
{
|
||||
walletid: id,
|
||||
tx: JSON.stringify(tx),
|
||||
},
|
||||
Realm.UpdateMode.Modified,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/// ########################################################################################################
|
||||
|
||||
if (walletToSave._txs_by_external_index) {
|
||||
realm.write(() => {
|
||||
// cleanup all existing transactions for the wallet first
|
||||
const walletTransactionsToDelete = realm.objects('WalletTransactions').filtered(`walletid = '${id}'`);
|
||||
realm.delete(walletTransactionsToDelete);
|
||||
|
||||
// insert new ones:
|
||||
for (const index of Object.keys(walletToSave._txs_by_external_index)) {
|
||||
// @ts-ignore index is number
|
||||
const txs = walletToSave._txs_by_external_index[index];
|
||||
for (const tx of txs) {
|
||||
realm.create(
|
||||
'WalletTransactions',
|
||||
{
|
||||
walletid: id,
|
||||
internal: false,
|
||||
index: parseInt(index, 10),
|
||||
tx: JSON.stringify(tx),
|
||||
},
|
||||
Realm.UpdateMode.Modified,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const index of Object.keys(walletToSave._txs_by_internal_index)) {
|
||||
// @ts-ignore index is number
|
||||
const txs = walletToSave._txs_by_internal_index[index];
|
||||
for (const tx of txs) {
|
||||
realm.create(
|
||||
'WalletTransactions',
|
||||
{
|
||||
walletid: id,
|
||||
internal: true,
|
||||
index: parseInt(index, 10),
|
||||
tx: JSON.stringify(tx),
|
||||
},
|
||||
Realm.UpdateMode.Modified,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(): Promise<void> {
|
||||
if (savingInProgress) {
|
||||
console.warn('saveToDisk is in progress');
|
||||
if (++savingInProgress > 10) presentAlert({ message: 'Critical error. Last actions were not saved' }); // should never happen
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * savingInProgress)); // sleep
|
||||
return this.saveToDisk();
|
||||
}
|
||||
savingInProgress = 1;
|
||||
|
||||
try {
|
||||
const walletsToSave = [];
|
||||
let realm;
|
||||
try {
|
||||
realm = await this.getRealm();
|
||||
} catch (error: any) {
|
||||
presentAlert({ message: error.message });
|
||||
}
|
||||
for (const key of this.wallets) {
|
||||
if (typeof key === 'boolean') continue;
|
||||
key.prepareForSerialization();
|
||||
// @ts-ignore wtf is wallet.current? Does it even exist?
|
||||
delete key.current;
|
||||
const keyCloned = Object.assign({}, key); // stripped-down version of a wallet to save to secure keystore
|
||||
if ('_hdWalletInstance' in key) {
|
||||
const k = keyCloned as any & WatchOnlyWallet;
|
||||
k._hdWalletInstance = Object.assign({}, key._hdWalletInstance);
|
||||
k._hdWalletInstance._txs_by_external_index = {};
|
||||
k._hdWalletInstance._txs_by_internal_index = {};
|
||||
}
|
||||
if (realm) this.offloadWalletToRealm(realm, key);
|
||||
// stripping down:
|
||||
if (key._txs_by_external_index) {
|
||||
keyCloned._txs_by_external_index = {};
|
||||
keyCloned._txs_by_internal_index = {};
|
||||
}
|
||||
|
||||
if ('_bip47_instance' in keyCloned) {
|
||||
delete keyCloned._bip47_instance; // since it wont be restored into a proper class instance
|
||||
}
|
||||
|
||||
walletsToSave.push(JSON.stringify({ ...keyCloned, type: keyCloned.type }));
|
||||
}
|
||||
if (realm) realm.close();
|
||||
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.getItemWithFallbackToRealm('data');
|
||||
buckets = JSON.parse(buckets);
|
||||
const newData = [];
|
||||
let num = 0;
|
||||
for (const bucket of buckets) {
|
||||
let decrypted;
|
||||
// if we had `usedBucketNum` during loadFromDisk(), no point to try to decode each bucket to find the one we
|
||||
// need, we just to find bucket with the same index
|
||||
if (usedBucketNum !== false) {
|
||||
if (num === usedBucketNum) {
|
||||
decrypted = true;
|
||||
}
|
||||
num++;
|
||||
} else {
|
||||
// we dont have `usedBucketNum` for whatever reason, so lets try to decrypt each bucket after bucket
|
||||
// till we find the right one
|
||||
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));
|
||||
}
|
||||
}
|
||||
// @ts-ignore bla bla bla
|
||||
data = newData;
|
||||
}
|
||||
|
||||
await this.setItem('data', JSON.stringify(data));
|
||||
await this.setItem(AppStorage.FLAG_ENCRYPTED, this.cachedPassword ? '1' : '');
|
||||
|
||||
// now, backing up same data in realm:
|
||||
const realmkeyValue = await this.openRealmKeyValue();
|
||||
this.saveToRealmKeyValue(realmkeyValue, 'data', JSON.stringify(data));
|
||||
this.saveToRealmKeyValue(realmkeyValue, AppStorage.FLAG_ENCRYPTED, this.cachedPassword ? '1' : '');
|
||||
realmkeyValue.close();
|
||||
} catch (error: any) {
|
||||
console.error('save to disk exception:', error.message);
|
||||
presentAlert({ message: 'save to disk exception: ' + error.message });
|
||||
if (error.message.includes('Realm file decryption failed')) {
|
||||
console.warn('purging realm key-value database file');
|
||||
this.purgeRealmKeyValueFile();
|
||||
}
|
||||
} finally {
|
||||
savingInProgress = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
fetchWalletBalances = async (index?: number): Promise<void> => {
|
||||
console.log('fetchWalletBalances for wallet#', typeof index === 'undefined' ? '(all)' : index);
|
||||
if (index || index === 0) {
|
||||
let c = 0;
|
||||
for (const wallet of this.wallets) {
|
||||
if (c++ === index) {
|
||||
await wallet.fetchBalance();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const wallet of this.wallets) {
|
||||
console.log('fetching balance for', wallet.getLabel());
|
||||
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>}
|
||||
*/
|
||||
fetchWalletTransactions = async (index?: number) => {
|
||||
console.log('fetchWalletTransactions for wallet#', typeof index === 'undefined' ? '(all)' : index);
|
||||
if (index || index === 0) {
|
||||
let c = 0;
|
||||
for (const wallet of this.wallets) {
|
||||
if (c++ === index) {
|
||||
await wallet.fetchTransactions();
|
||||
|
||||
if ('fetchPendingTransactions' in wallet) {
|
||||
await wallet.fetchPendingTransactions();
|
||||
await wallet.fetchUserInvoices();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const wallet of this.wallets) {
|
||||
await wallet.fetchTransactions();
|
||||
if ('fetchPendingTransactions' in wallet) {
|
||||
await wallet.fetchPendingTransactions();
|
||||
await wallet.fetchUserInvoices();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchSenderPaymentCodes = async (index?: number) => {
|
||||
console.log('fetchSenderPaymentCodes for wallet#', typeof index === 'undefined' ? '(all)' : index);
|
||||
if (index || index === 0) {
|
||||
const wallet = this.wallets[index];
|
||||
try {
|
||||
if (!(wallet.allowBIP47() && wallet.isBIP47Enabled() && 'fetchBIP47SenderPaymentCodes' in wallet)) return;
|
||||
await wallet.fetchBIP47SenderPaymentCodes();
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch sender payment codes for wallet', index, error);
|
||||
}
|
||||
} else {
|
||||
for (const wallet of this.wallets) {
|
||||
try {
|
||||
if (!(wallet.allowBIP47() && wallet.isBIP47Enabled() && 'fetchBIP47SenderPaymentCodes' in wallet)) continue;
|
||||
await wallet.fetchBIP47SenderPaymentCodes();
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch sender payment codes for wallet', wallet.label, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getWallets = (): TWallet[] => {
|
||||
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.
|
||||
* @param includeWalletsWithHideTransactionsEnabled {Boolean} Wallets' _hideTransactionsInWalletsList property determines wether the user wants this wallet's txs hidden from the main list view.
|
||||
*/
|
||||
getTransactions = (
|
||||
index?: number,
|
||||
limit: number = Infinity,
|
||||
includeWalletsWithHideTransactionsEnabled: boolean = false,
|
||||
): Transaction[] => {
|
||||
if (index || index === 0) {
|
||||
let txs: Transaction[] = [];
|
||||
let c = 0;
|
||||
for (const wallet of this.wallets) {
|
||||
if (c++ === index) {
|
||||
txs = txs.concat(wallet.getTransactions());
|
||||
}
|
||||
}
|
||||
return txs;
|
||||
}
|
||||
|
||||
let txs: Transaction[] = [];
|
||||
for (const wallet of this.wallets.filter(w => includeWalletsWithHideTransactionsEnabled || !w.getHideTransactionsInWalletsList())) {
|
||||
const walletTransactions = wallet.getTransactions();
|
||||
const walletID = wallet.getID();
|
||||
for (const t of walletTransactions) {
|
||||
t.walletPreferredBalanceUnit = wallet.getPreferredBalanceUnit();
|
||||
t.walletID = walletID;
|
||||
}
|
||||
txs = txs.concat(walletTransactions);
|
||||
}
|
||||
|
||||
return txs
|
||||
.sort((a, b) => {
|
||||
const bTime = new Date(b.received!).getTime();
|
||||
const aTime = new Date(a.received!).getTime();
|
||||
return bTime - aTime;
|
||||
})
|
||||
.slice(0, limit);
|
||||
};
|
||||
|
||||
/**
|
||||
* Getter for a sum of all balances of all wallets
|
||||
*/
|
||||
getBalance = (): number => {
|
||||
let finalBalance = 0;
|
||||
for (const wal of this.wallets) {
|
||||
finalBalance += wal.getBalance();
|
||||
}
|
||||
return finalBalance;
|
||||
};
|
||||
|
||||
isAdvancedModeEnabled = async (): Promise<boolean> => {
|
||||
try {
|
||||
return !!(await AsyncStorage.getItem(AppStorage.ADVANCED_MODE_ENABLED));
|
||||
} catch (_) {}
|
||||
return false;
|
||||
};
|
||||
|
||||
setIsAdvancedModeEnabled = async (value: boolean) => {
|
||||
await AsyncStorage.setItem(AppStorage.ADVANCED_MODE_ENABLED, value ? '1' : '');
|
||||
};
|
||||
|
||||
isHandoffEnabled = async (): Promise<boolean> => {
|
||||
try {
|
||||
return !!(await AsyncStorage.getItem(AppStorage.HANDOFF_STORAGE_KEY));
|
||||
} catch (_) {}
|
||||
return false;
|
||||
};
|
||||
|
||||
setIsHandoffEnabled = async (value: boolean): Promise<void> => {
|
||||
await AsyncStorage.setItem(AppStorage.HANDOFF_STORAGE_KEY, value ? '1' : '');
|
||||
};
|
||||
|
||||
isDoNotTrackEnabled = async (): Promise<boolean> => {
|
||||
try {
|
||||
const keyExists = await AsyncStorage.getItem(AppStorage.DO_NOT_TRACK);
|
||||
if (keyExists !== null) {
|
||||
const doNotTrackValue = !!keyExists;
|
||||
if (doNotTrackValue) {
|
||||
await DefaultPreference.setName('group.io.bluewallet.bluewallet');
|
||||
await DefaultPreference.set(AppStorage.DO_NOT_TRACK, '1');
|
||||
AsyncStorage.removeItem(AppStorage.DO_NOT_TRACK);
|
||||
} else {
|
||||
return Boolean(await DefaultPreference.get(AppStorage.DO_NOT_TRACK));
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
const doNotTrackValue = await DefaultPreference.get(AppStorage.DO_NOT_TRACK);
|
||||
return doNotTrackValue === '1' || false;
|
||||
};
|
||||
|
||||
setDoNotTrack = async (value: boolean) => {
|
||||
await DefaultPreference.setName('group.io.bluewallet.bluewallet');
|
||||
if (value) {
|
||||
await DefaultPreference.set(AppStorage.DO_NOT_TRACK, '1');
|
||||
} else {
|
||||
await DefaultPreference.clear(AppStorage.DO_NOT_TRACK);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Simple async sleeper function
|
||||
*/
|
||||
sleep = (ms: number): Promise<void> => {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
};
|
||||
|
||||
purgeRealmKeyValueFile() {
|
||||
const path = 'keyvalue.realm';
|
||||
return Realm.deleteFile({
|
||||
path,
|
||||
});
|
||||
}
|
||||
|
||||
async moveRealmFilesToCacheDirectory() {
|
||||
const documentPath = RNFS.DocumentDirectoryPath; // Path to documentPath folder
|
||||
const cachePath = RNFS.CachesDirectoryPath; // Path to cachePath folder
|
||||
try {
|
||||
if (!(await RNFS.exists(documentPath))) return; // If the documentPath directory does not exist, return (nothing to move)
|
||||
const files = await RNFS.readDir(documentPath); // Read all files in documentPath directory
|
||||
if (Array.isArray(files) && files.length === 0) return; // If there are no files, return (nothing to move)
|
||||
const appRealmFiles = files.filter(
|
||||
file => file.name.endsWith('.realm') || file.name.endsWith('.realm.lock') || file.name.includes('.realm.management'),
|
||||
);
|
||||
|
||||
for (const file of appRealmFiles) {
|
||||
const filePath = `${documentPath}/${file.name}`;
|
||||
const newFilePath = `${cachePath}/${file.name}`;
|
||||
const fileExists = await RNFS.exists(filePath); // Check if the file exists
|
||||
const cacheFileExists = await RNFS.exists(newFilePath); // Check if the file already exists in the cache directory
|
||||
|
||||
if (fileExists) {
|
||||
if (cacheFileExists) {
|
||||
await RNFS.unlink(newFilePath); // Delete the file in the cache directory if it exists
|
||||
console.log(`Existing file removed from cache: ${newFilePath}`);
|
||||
}
|
||||
await RNFS.moveFile(filePath, newFilePath); // Move the file
|
||||
console.log(`Moved Realm file: ${filePath} to ${newFilePath}`);
|
||||
} else {
|
||||
console.log(`File does not exist: ${filePath}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error moving Realm files:', error);
|
||||
throw new Error(`Error moving Realm files: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
import { AppStorage } from './class/';
|
||||
|
||||
const BlueApp = new AppStorage();
|
||||
// If attempt reaches 10, a wipe keychain option will be provided to the user.
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { createContext, useEffect, useState } from 'react';
|
||||
import { useAsyncStorage } from '@react-native-async-storage/async-storage';
|
||||
|
||||
import BlueApp, { TTXMetadata, startAndDecrypt } from '../BlueApp';
|
||||
import BlueApp, { startAndDecrypt } from '../BlueApp';
|
||||
import Notifications from '../blue_modules/notifications';
|
||||
import { LegacyWallet, WatchOnlyWallet } from '../class';
|
||||
import { LegacyWallet, TTXMetadata, WatchOnlyWallet } from '../class';
|
||||
import type { TWallet } from '../class/wallets/types';
|
||||
import presentAlert from '../components/Alert';
|
||||
import loc, { STORAGE_KEY as LOC_STORAGE_KEY } from '../loc';
|
||||
|
931
class/app-storage.ts
Normal file
931
class/app-storage.ts
Normal file
@ -0,0 +1,931 @@
|
||||
import { Transaction, TWallet } from './wallets/types';
|
||||
import RNSecureKeyStore, { ACCESSIBLE } from 'react-native-secure-key-store';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import * as encryption from '../blue_modules/encryption';
|
||||
import createHash from 'create-hash';
|
||||
import RNFS from 'react-native-fs';
|
||||
import Realm from 'realm';
|
||||
import Keychain from 'react-native-keychain';
|
||||
import { randomBytes } from './rng';
|
||||
import presentAlert from '../components/Alert';
|
||||
import { SegwitBech32Wallet } from './wallets/segwit-bech32-wallet';
|
||||
import { SegwitP2SHWallet } from './wallets/segwit-p2sh-wallet';
|
||||
import { WatchOnlyWallet } from './wallets/watch-only-wallet';
|
||||
import { HDLegacyP2PKHWallet } from './wallets/hd-legacy-p2pkh-wallet';
|
||||
import { HDSegwitP2SHWallet } from './wallets/hd-segwit-p2sh-wallet';
|
||||
import { HDSegwitBech32Wallet } from './wallets/hd-segwit-bech32-wallet';
|
||||
import { HDLegacyBreadwalletWallet } from './wallets/hd-legacy-breadwallet-wallet';
|
||||
import { HDLegacyElectrumSeedP2PKHWallet } from './wallets/hd-legacy-electrum-seed-p2pkh-wallet';
|
||||
import { HDSegwitElectrumSeedP2WPKHWallet } from './wallets/hd-segwit-electrum-seed-p2wpkh-wallet';
|
||||
import { MultisigHDWallet } from './wallets/multisig-hd-wallet';
|
||||
import { HDAezeedWallet } from './wallets/hd-aezeed-wallet';
|
||||
import { LightningLdkWallet } from './wallets/lightning-ldk-wallet';
|
||||
import { SLIP39LegacyP2PKHWallet, SLIP39SegwitBech32Wallet, SLIP39SegwitP2SHWallet } from './wallets/slip39-wallets';
|
||||
import { LightningCustodianWallet } from './wallets/lightning-custodian-wallet';
|
||||
import { LegacyWallet } from './wallets/legacy-wallet';
|
||||
import DefaultPreference from 'react-native-default-preference';
|
||||
|
||||
let usedBucketNum: boolean | number = false;
|
||||
let savingInProgress = 0; // its both a flag and a counter of attempts to write to disk
|
||||
|
||||
export type TTXMetadata = {
|
||||
[txid: string]: {
|
||||
memo?: string;
|
||||
txhex?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type TRealmTransaction = {
|
||||
internal: boolean;
|
||||
index: number;
|
||||
tx: string;
|
||||
};
|
||||
|
||||
const isReactNative = typeof navigator !== 'undefined' && navigator?.product === 'ReactNative';
|
||||
|
||||
export class AppStorage {
|
||||
static FLAG_ENCRYPTED = 'data_encrypted';
|
||||
static LNDHUB = 'lndhub';
|
||||
static ADVANCED_MODE_ENABLED = 'advancedmodeenabled';
|
||||
static DO_NOT_TRACK = 'donottrack';
|
||||
static HANDOFF_STORAGE_KEY = 'HandOff';
|
||||
|
||||
static keys2migrate = [AppStorage.HANDOFF_STORAGE_KEY, AppStorage.DO_NOT_TRACK, AppStorage.ADVANCED_MODE_ENABLED];
|
||||
|
||||
public cachedPassword?: false | string;
|
||||
public tx_metadata: TTXMetadata;
|
||||
public wallets: TWallet[];
|
||||
|
||||
constructor() {
|
||||
this.wallets = [];
|
||||
this.tx_metadata = {};
|
||||
this.cachedPassword = false;
|
||||
}
|
||||
|
||||
async migrateKeys() {
|
||||
// do not migrate keys if we are not in RN env
|
||||
if (!isReactNative) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key of AppStorage.keys2migrate) {
|
||||
try {
|
||||
const value = await RNSecureKeyStore.get(key);
|
||||
if (value) {
|
||||
await AsyncStorage.setItem(key, value);
|
||||
await RNSecureKeyStore.remove(key);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for storage call. Secure store works only in RN environment. AsyncStorage is
|
||||
* used for cli/tests
|
||||
*/
|
||||
setItem = (key: string, value: any): Promise<any> => {
|
||||
if (isReactNative) {
|
||||
return RNSecureKeyStore.set(key, value, { accessible: ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY });
|
||||
} else {
|
||||
return AsyncStorage.setItem(key, value);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper for storage call. Secure store works only in RN environment. AsyncStorage is
|
||||
* used for cli/tests
|
||||
*/
|
||||
getItem = (key: string): Promise<any> => {
|
||||
if (isReactNative) {
|
||||
return RNSecureKeyStore.get(key);
|
||||
} else {
|
||||
return AsyncStorage.getItem(key);
|
||||
}
|
||||
};
|
||||
|
||||
getItemWithFallbackToRealm = async (key: string): Promise<any | null> => {
|
||||
let value;
|
||||
try {
|
||||
return await this.getItem(key);
|
||||
} catch (error: any) {
|
||||
console.warn('error reading', key, error.message);
|
||||
console.warn('fallback to realm');
|
||||
const realmKeyValue = await this.openRealmKeyValue();
|
||||
const obj = realmKeyValue.objectForPrimaryKey('KeyValue', key); // search for a realm object with a primary key
|
||||
value = obj?.value;
|
||||
realmKeyValue.close();
|
||||
if (value) {
|
||||
// @ts-ignore value.length
|
||||
console.warn('successfully recovered', value.length, 'bytes from realm for key', key);
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
storageIsEncrypted = async (): Promise<boolean> => {
|
||||
let data;
|
||||
try {
|
||||
data = await this.getItemWithFallbackToRealm(AppStorage.FLAG_ENCRYPTED);
|
||||
} catch (error: any) {
|
||||
console.warn('error reading `' + AppStorage.FLAG_ENCRYPTED + '` key:', error.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(data);
|
||||
};
|
||||
|
||||
isPasswordInUse = async (password: string) => {
|
||||
try {
|
||||
let data = await this.getItem('data');
|
||||
data = this.decryptData(data, password);
|
||||
return Boolean(data);
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Iterates through all values of `data` trying to
|
||||
* decrypt each one, and returns first one successfully decrypted
|
||||
*/
|
||||
decryptData(data: string, password: string): boolean | string {
|
||||
data = JSON.parse(data);
|
||||
let decrypted;
|
||||
let num = 0;
|
||||
for (const value of data) {
|
||||
decrypted = encryption.decrypt(value, password);
|
||||
|
||||
if (decrypted) {
|
||||
usedBucketNum = num;
|
||||
return decrypted;
|
||||
}
|
||||
num++;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
decryptStorage = async (password: string): Promise<boolean> => {
|
||||
if (password === this.cachedPassword) {
|
||||
this.cachedPassword = undefined;
|
||||
await this.saveToDisk();
|
||||
this.wallets = [];
|
||||
this.tx_metadata = {};
|
||||
return this.loadFromDisk();
|
||||
} else {
|
||||
throw new Error('Incorrect password. Please, try again.');
|
||||
}
|
||||
};
|
||||
|
||||
encryptStorage = async (password: string): Promise<void> => {
|
||||
// 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)
|
||||
|
||||
const 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
|
||||
*/
|
||||
createFakeStorage = async (fakePassword: string): Promise<boolean> => {
|
||||
usedBucketNum = false; // resetting currently used bucket so we wont overwrite it
|
||||
this.wallets = [];
|
||||
this.tx_metadata = {};
|
||||
|
||||
const 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;
|
||||
};
|
||||
|
||||
hashIt = (s: string): string => {
|
||||
return createHash('sha256').update(s).digest().toString('hex');
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns instace of the Realm database, which is encrypted either by cached user's password OR default password.
|
||||
* Database file is deterministically derived from encryption key.
|
||||
*/
|
||||
async getRealm() {
|
||||
const cacheFolderPath = RNFS.CachesDirectoryPath; // Path to cache folder
|
||||
const password = this.hashIt(this.cachedPassword || 'fyegjitkyf[eqjnc.lf');
|
||||
const buf = Buffer.from(this.hashIt(password) + this.hashIt(password), 'hex');
|
||||
const encryptionKey = Int8Array.from(buf);
|
||||
const fileName = this.hashIt(this.hashIt(password)) + '-wallettransactions.realm';
|
||||
const path = `${cacheFolderPath}/${fileName}`; // Use cache folder path
|
||||
|
||||
const schema = [
|
||||
{
|
||||
name: 'WalletTransactions',
|
||||
properties: {
|
||||
walletid: { type: 'string', indexed: true },
|
||||
internal: 'bool?', // true - internal, false - external
|
||||
index: 'int?',
|
||||
tx: 'string', // stringified json
|
||||
},
|
||||
},
|
||||
];
|
||||
// @ts-ignore schema doesn't match Realm's schema type
|
||||
return Realm.open({
|
||||
// @ts-ignore schema doesn't match Realm's schema type
|
||||
schema,
|
||||
path,
|
||||
encryptionKey,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns instace of the Realm database, which is encrypted by device unique id
|
||||
* Database file is static.
|
||||
*
|
||||
* @returns {Promise<Realm>}
|
||||
*/
|
||||
async openRealmKeyValue(): Promise<Realm> {
|
||||
const cacheFolderPath = RNFS.CachesDirectoryPath; // Path to cache folder
|
||||
const service = 'realm_encryption_key';
|
||||
let password;
|
||||
const credentials = await Keychain.getGenericPassword({ service });
|
||||
if (credentials) {
|
||||
password = credentials.password;
|
||||
} else {
|
||||
const buf = await randomBytes(64);
|
||||
password = buf.toString('hex');
|
||||
await Keychain.setGenericPassword(service, password, { service });
|
||||
}
|
||||
|
||||
const buf = Buffer.from(password, 'hex');
|
||||
const encryptionKey = Int8Array.from(buf);
|
||||
const path = `${cacheFolderPath}/keyvalue.realm`; // Use cache folder path
|
||||
|
||||
const schema = [
|
||||
{
|
||||
name: 'KeyValue',
|
||||
primaryKey: 'key',
|
||||
properties: {
|
||||
key: { type: 'string', indexed: true },
|
||||
value: 'string', // stringified json, or whatever
|
||||
},
|
||||
},
|
||||
];
|
||||
// @ts-ignore schema doesn't match Realm's schema type
|
||||
return Realm.open({
|
||||
// @ts-ignore schema doesn't match Realm's schema type
|
||||
schema,
|
||||
path,
|
||||
encryptionKey,
|
||||
});
|
||||
}
|
||||
|
||||
saveToRealmKeyValue(realmkeyValue: Realm, key: string, value: any) {
|
||||
realmkeyValue.write(() => {
|
||||
realmkeyValue.create(
|
||||
'KeyValue',
|
||||
{
|
||||
key,
|
||||
value,
|
||||
},
|
||||
Realm.UpdateMode.Modified,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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?: string): Promise<boolean> {
|
||||
// Wrap inside a try so if anything goes wrong it wont block loadFromDisk from continuing
|
||||
try {
|
||||
await this.moveRealmFilesToCacheDirectory();
|
||||
} catch (error: any) {
|
||||
console.warn('moveRealmFilesToCacheDirectory error:', error.message);
|
||||
}
|
||||
let data = await this.getItemWithFallbackToRealm('data');
|
||||
if (password) {
|
||||
data = this.decryptData(data, password);
|
||||
if (data) {
|
||||
// password is good, cache it
|
||||
this.cachedPassword = password;
|
||||
}
|
||||
}
|
||||
if (data !== null) {
|
||||
let realm;
|
||||
try {
|
||||
realm = await this.getRealm();
|
||||
} catch (error: any) {
|
||||
presentAlert({ message: error.message });
|
||||
}
|
||||
data = JSON.parse(data);
|
||||
if (!data.wallets) return false;
|
||||
const wallets = data.wallets;
|
||||
for (const key of wallets) {
|
||||
// deciding which type is wallet and instatiating correct object
|
||||
const tempObj = JSON.parse(key);
|
||||
let unserializedWallet: TWallet;
|
||||
switch (tempObj.type) {
|
||||
case SegwitBech32Wallet.type:
|
||||
unserializedWallet = SegwitBech32Wallet.fromJson(key) as unknown as SegwitBech32Wallet;
|
||||
break;
|
||||
case SegwitP2SHWallet.type:
|
||||
unserializedWallet = SegwitP2SHWallet.fromJson(key) as unknown as SegwitP2SHWallet;
|
||||
break;
|
||||
case WatchOnlyWallet.type:
|
||||
unserializedWallet = WatchOnlyWallet.fromJson(key) as unknown as WatchOnlyWallet;
|
||||
unserializedWallet.init();
|
||||
if (unserializedWallet.isHd() && !unserializedWallet.isXpubValid()) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case HDLegacyP2PKHWallet.type:
|
||||
unserializedWallet = HDLegacyP2PKHWallet.fromJson(key) as unknown as HDLegacyP2PKHWallet;
|
||||
break;
|
||||
case HDSegwitP2SHWallet.type:
|
||||
unserializedWallet = HDSegwitP2SHWallet.fromJson(key) as unknown as HDSegwitP2SHWallet;
|
||||
break;
|
||||
case HDSegwitBech32Wallet.type:
|
||||
unserializedWallet = HDSegwitBech32Wallet.fromJson(key) as unknown as HDSegwitBech32Wallet;
|
||||
break;
|
||||
case HDLegacyBreadwalletWallet.type:
|
||||
unserializedWallet = HDLegacyBreadwalletWallet.fromJson(key) as unknown as HDLegacyBreadwalletWallet;
|
||||
break;
|
||||
case HDLegacyElectrumSeedP2PKHWallet.type:
|
||||
unserializedWallet = HDLegacyElectrumSeedP2PKHWallet.fromJson(key) as unknown as HDLegacyElectrumSeedP2PKHWallet;
|
||||
break;
|
||||
case HDSegwitElectrumSeedP2WPKHWallet.type:
|
||||
unserializedWallet = HDSegwitElectrumSeedP2WPKHWallet.fromJson(key) as unknown as HDSegwitElectrumSeedP2WPKHWallet;
|
||||
break;
|
||||
case MultisigHDWallet.type:
|
||||
unserializedWallet = MultisigHDWallet.fromJson(key) as unknown as MultisigHDWallet;
|
||||
break;
|
||||
case HDAezeedWallet.type:
|
||||
unserializedWallet = HDAezeedWallet.fromJson(key) as unknown as HDAezeedWallet;
|
||||
// migrate password to this.passphrase field
|
||||
// remove this code somewhere in year 2022
|
||||
if (unserializedWallet.secret.includes(':')) {
|
||||
const [mnemonic, passphrase] = unserializedWallet.secret.split(':');
|
||||
unserializedWallet.secret = mnemonic;
|
||||
unserializedWallet.passphrase = passphrase;
|
||||
}
|
||||
|
||||
break;
|
||||
case LightningLdkWallet.type:
|
||||
unserializedWallet = LightningLdkWallet.fromJson(key) as unknown as LightningLdkWallet;
|
||||
break;
|
||||
case SLIP39SegwitP2SHWallet.type:
|
||||
unserializedWallet = SLIP39SegwitP2SHWallet.fromJson(key) as unknown as SLIP39SegwitP2SHWallet;
|
||||
break;
|
||||
case SLIP39LegacyP2PKHWallet.type:
|
||||
unserializedWallet = SLIP39LegacyP2PKHWallet.fromJson(key) as unknown as SLIP39LegacyP2PKHWallet;
|
||||
break;
|
||||
case SLIP39SegwitBech32Wallet.type:
|
||||
unserializedWallet = SLIP39SegwitBech32Wallet.fromJson(key) as unknown as SLIP39SegwitBech32Wallet;
|
||||
break;
|
||||
case LightningCustodianWallet.type: {
|
||||
unserializedWallet = LightningCustodianWallet.fromJson(key) as unknown as LightningCustodianWallet;
|
||||
let lndhub: false | any = 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('wallet does not have a baseURI. Continuing init...');
|
||||
}
|
||||
unserializedWallet.init();
|
||||
break;
|
||||
}
|
||||
case LegacyWallet.type:
|
||||
default:
|
||||
unserializedWallet = LegacyWallet.fromJson(key) as unknown as LegacyWallet;
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
if (realm) this.inflateWalletFromRealm(realm, unserializedWallet);
|
||||
} catch (error: any) {
|
||||
presentAlert({ message: error.message });
|
||||
}
|
||||
|
||||
// done
|
||||
const ID = unserializedWallet.getID();
|
||||
if (!this.wallets.some(wallet => wallet.getID() === ID)) {
|
||||
this.wallets.push(unserializedWallet);
|
||||
this.tx_metadata = data.tx_metadata;
|
||||
}
|
||||
}
|
||||
if (realm) realm.close();
|
||||
return true;
|
||||
} else {
|
||||
return false; // failed loading data or loading/decryptin data
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup wallet in list by it's secret and
|
||||
* remove it from `this.wallets`
|
||||
*
|
||||
* @param wallet {AbstractWallet}
|
||||
*/
|
||||
deleteWallet = (wallet: TWallet): void => {
|
||||
const ID = wallet.getID();
|
||||
const tempWallets = [];
|
||||
|
||||
if (wallet.type === LightningLdkWallet.type) {
|
||||
const ldkwallet = wallet;
|
||||
ldkwallet.stop().then(ldkwallet.purgeLocalStorage).catch(alert);
|
||||
}
|
||||
|
||||
for (const value of this.wallets) {
|
||||
if (value.getID() === ID) {
|
||||
// the one we should delete
|
||||
// nop
|
||||
} else {
|
||||
// the one we must keep
|
||||
tempWallets.push(value);
|
||||
}
|
||||
}
|
||||
this.wallets = tempWallets;
|
||||
};
|
||||
|
||||
inflateWalletFromRealm(realm: Realm, walletToInflate: TWallet) {
|
||||
const transactions = realm.objects('WalletTransactions');
|
||||
const transactionsForWallet = transactions.filtered(`walletid = "${walletToInflate.getID()}"`) as unknown as TRealmTransaction[];
|
||||
for (const tx of transactionsForWallet) {
|
||||
if (tx.internal === false) {
|
||||
if ('_hdWalletInstance' in walletToInflate && walletToInflate._hdWalletInstance) {
|
||||
const hd = walletToInflate._hdWalletInstance;
|
||||
hd._txs_by_external_index[tx.index] = hd._txs_by_external_index[tx.index] || [];
|
||||
const transaction = JSON.parse(tx.tx);
|
||||
hd._txs_by_external_index[tx.index].push(transaction);
|
||||
} else {
|
||||
walletToInflate._txs_by_external_index[tx.index] = walletToInflate._txs_by_external_index[tx.index] || [];
|
||||
const transaction = JSON.parse(tx.tx);
|
||||
(walletToInflate._txs_by_external_index[tx.index] as Transaction[]).push(transaction);
|
||||
}
|
||||
} else if (tx.internal === true) {
|
||||
if ('_hdWalletInstance' in walletToInflate && walletToInflate._hdWalletInstance) {
|
||||
const hd = walletToInflate._hdWalletInstance;
|
||||
hd._txs_by_internal_index[tx.index] = hd._txs_by_internal_index[tx.index] || [];
|
||||
const transaction = JSON.parse(tx.tx);
|
||||
hd._txs_by_internal_index[tx.index].push(transaction);
|
||||
} else {
|
||||
walletToInflate._txs_by_internal_index[tx.index] = walletToInflate._txs_by_internal_index[tx.index] || [];
|
||||
const transaction = JSON.parse(tx.tx);
|
||||
(walletToInflate._txs_by_internal_index[tx.index] as Transaction[]).push(transaction);
|
||||
}
|
||||
} else {
|
||||
if (!Array.isArray(walletToInflate._txs_by_external_index)) walletToInflate._txs_by_external_index = [];
|
||||
walletToInflate._txs_by_external_index = walletToInflate._txs_by_external_index || [];
|
||||
const transaction = JSON.parse(tx.tx);
|
||||
(walletToInflate._txs_by_external_index as Transaction[]).push(transaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
offloadWalletToRealm(realm: Realm, wallet: TWallet): void {
|
||||
const id = wallet.getID();
|
||||
const walletToSave = ('_hdWalletInstance' in wallet && wallet._hdWalletInstance) || wallet;
|
||||
|
||||
if (Array.isArray(walletToSave._txs_by_external_index)) {
|
||||
// if this var is an array that means its a single-address wallet class, and this var is a flat array
|
||||
// with transactions
|
||||
realm.write(() => {
|
||||
// cleanup all existing transactions for the wallet first
|
||||
const walletTransactionsToDelete = realm.objects('WalletTransactions').filtered(`walletid = '${id}'`);
|
||||
realm.delete(walletTransactionsToDelete);
|
||||
|
||||
// @ts-ignore walletToSave._txs_by_external_index is array
|
||||
for (const tx of walletToSave._txs_by_external_index) {
|
||||
realm.create(
|
||||
'WalletTransactions',
|
||||
{
|
||||
walletid: id,
|
||||
tx: JSON.stringify(tx),
|
||||
},
|
||||
Realm.UpdateMode.Modified,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/// ########################################################################################################
|
||||
|
||||
if (walletToSave._txs_by_external_index) {
|
||||
realm.write(() => {
|
||||
// cleanup all existing transactions for the wallet first
|
||||
const walletTransactionsToDelete = realm.objects('WalletTransactions').filtered(`walletid = '${id}'`);
|
||||
realm.delete(walletTransactionsToDelete);
|
||||
|
||||
// insert new ones:
|
||||
for (const index of Object.keys(walletToSave._txs_by_external_index)) {
|
||||
// @ts-ignore index is number
|
||||
const txs = walletToSave._txs_by_external_index[index];
|
||||
for (const tx of txs) {
|
||||
realm.create(
|
||||
'WalletTransactions',
|
||||
{
|
||||
walletid: id,
|
||||
internal: false,
|
||||
index: parseInt(index, 10),
|
||||
tx: JSON.stringify(tx),
|
||||
},
|
||||
Realm.UpdateMode.Modified,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const index of Object.keys(walletToSave._txs_by_internal_index)) {
|
||||
// @ts-ignore index is number
|
||||
const txs = walletToSave._txs_by_internal_index[index];
|
||||
for (const tx of txs) {
|
||||
realm.create(
|
||||
'WalletTransactions',
|
||||
{
|
||||
walletid: id,
|
||||
internal: true,
|
||||
index: parseInt(index, 10),
|
||||
tx: JSON.stringify(tx),
|
||||
},
|
||||
Realm.UpdateMode.Modified,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(): Promise<void> {
|
||||
if (savingInProgress) {
|
||||
console.warn('saveToDisk is in progress');
|
||||
if (++savingInProgress > 10) presentAlert({ message: 'Critical error. Last actions were not saved' }); // should never happen
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * savingInProgress)); // sleep
|
||||
return this.saveToDisk();
|
||||
}
|
||||
savingInProgress = 1;
|
||||
|
||||
try {
|
||||
const walletsToSave = [];
|
||||
let realm;
|
||||
try {
|
||||
realm = await this.getRealm();
|
||||
} catch (error: any) {
|
||||
presentAlert({ message: error.message });
|
||||
}
|
||||
for (const key of this.wallets) {
|
||||
if (typeof key === 'boolean') continue;
|
||||
key.prepareForSerialization();
|
||||
// @ts-ignore wtf is wallet.current? Does it even exist?
|
||||
delete key.current;
|
||||
const keyCloned = Object.assign({}, key); // stripped-down version of a wallet to save to secure keystore
|
||||
if ('_hdWalletInstance' in key) {
|
||||
const k = keyCloned as any & WatchOnlyWallet;
|
||||
k._hdWalletInstance = Object.assign({}, key._hdWalletInstance);
|
||||
k._hdWalletInstance._txs_by_external_index = {};
|
||||
k._hdWalletInstance._txs_by_internal_index = {};
|
||||
}
|
||||
if (realm) this.offloadWalletToRealm(realm, key);
|
||||
// stripping down:
|
||||
if (key._txs_by_external_index) {
|
||||
keyCloned._txs_by_external_index = {};
|
||||
keyCloned._txs_by_internal_index = {};
|
||||
}
|
||||
|
||||
if ('_bip47_instance' in keyCloned) {
|
||||
delete keyCloned._bip47_instance; // since it wont be restored into a proper class instance
|
||||
}
|
||||
|
||||
walletsToSave.push(JSON.stringify({ ...keyCloned, type: keyCloned.type }));
|
||||
}
|
||||
if (realm) realm.close();
|
||||
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.getItemWithFallbackToRealm('data');
|
||||
buckets = JSON.parse(buckets);
|
||||
const newData = [];
|
||||
let num = 0;
|
||||
for (const bucket of buckets) {
|
||||
let decrypted;
|
||||
// if we had `usedBucketNum` during loadFromDisk(), no point to try to decode each bucket to find the one we
|
||||
// need, we just to find bucket with the same index
|
||||
if (usedBucketNum !== false) {
|
||||
if (num === usedBucketNum) {
|
||||
decrypted = true;
|
||||
}
|
||||
num++;
|
||||
} else {
|
||||
// we dont have `usedBucketNum` for whatever reason, so lets try to decrypt each bucket after bucket
|
||||
// till we find the right one
|
||||
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));
|
||||
}
|
||||
}
|
||||
// @ts-ignore bla bla bla
|
||||
data = newData;
|
||||
}
|
||||
|
||||
await this.setItem('data', JSON.stringify(data));
|
||||
await this.setItem(AppStorage.FLAG_ENCRYPTED, this.cachedPassword ? '1' : '');
|
||||
|
||||
// now, backing up same data in realm:
|
||||
const realmkeyValue = await this.openRealmKeyValue();
|
||||
this.saveToRealmKeyValue(realmkeyValue, 'data', JSON.stringify(data));
|
||||
this.saveToRealmKeyValue(realmkeyValue, AppStorage.FLAG_ENCRYPTED, this.cachedPassword ? '1' : '');
|
||||
realmkeyValue.close();
|
||||
} catch (error: any) {
|
||||
console.error('save to disk exception:', error.message);
|
||||
presentAlert({ message: 'save to disk exception: ' + error.message });
|
||||
if (error.message.includes('Realm file decryption failed')) {
|
||||
console.warn('purging realm key-value database file');
|
||||
this.purgeRealmKeyValueFile();
|
||||
}
|
||||
} finally {
|
||||
savingInProgress = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
fetchWalletBalances = async (index?: number): Promise<void> => {
|
||||
console.log('fetchWalletBalances for wallet#', typeof index === 'undefined' ? '(all)' : index);
|
||||
if (index || index === 0) {
|
||||
let c = 0;
|
||||
for (const wallet of this.wallets) {
|
||||
if (c++ === index) {
|
||||
await wallet.fetchBalance();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const wallet of this.wallets) {
|
||||
console.log('fetching balance for', wallet.getLabel());
|
||||
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>}
|
||||
*/
|
||||
fetchWalletTransactions = async (index?: number) => {
|
||||
console.log('fetchWalletTransactions for wallet#', typeof index === 'undefined' ? '(all)' : index);
|
||||
if (index || index === 0) {
|
||||
let c = 0;
|
||||
for (const wallet of this.wallets) {
|
||||
if (c++ === index) {
|
||||
await wallet.fetchTransactions();
|
||||
|
||||
if ('fetchPendingTransactions' in wallet) {
|
||||
await wallet.fetchPendingTransactions();
|
||||
await wallet.fetchUserInvoices();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const wallet of this.wallets) {
|
||||
await wallet.fetchTransactions();
|
||||
if ('fetchPendingTransactions' in wallet) {
|
||||
await wallet.fetchPendingTransactions();
|
||||
await wallet.fetchUserInvoices();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchSenderPaymentCodes = async (index?: number) => {
|
||||
console.log('fetchSenderPaymentCodes for wallet#', typeof index === 'undefined' ? '(all)' : index);
|
||||
if (index || index === 0) {
|
||||
const wallet = this.wallets[index];
|
||||
try {
|
||||
if (!(wallet.allowBIP47() && wallet.isBIP47Enabled() && 'fetchBIP47SenderPaymentCodes' in wallet)) return;
|
||||
await wallet.fetchBIP47SenderPaymentCodes();
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch sender payment codes for wallet', index, error);
|
||||
}
|
||||
} else {
|
||||
for (const wallet of this.wallets) {
|
||||
try {
|
||||
if (!(wallet.allowBIP47() && wallet.isBIP47Enabled() && 'fetchBIP47SenderPaymentCodes' in wallet)) continue;
|
||||
await wallet.fetchBIP47SenderPaymentCodes();
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch sender payment codes for wallet', wallet.label, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getWallets = (): TWallet[] => {
|
||||
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.
|
||||
* @param includeWalletsWithHideTransactionsEnabled {Boolean} Wallets' _hideTransactionsInWalletsList property determines wether the user wants this wallet's txs hidden from the main list view.
|
||||
*/
|
||||
getTransactions = (
|
||||
index?: number,
|
||||
limit: number = Infinity,
|
||||
includeWalletsWithHideTransactionsEnabled: boolean = false,
|
||||
): Transaction[] => {
|
||||
if (index || index === 0) {
|
||||
let txs: Transaction[] = [];
|
||||
let c = 0;
|
||||
for (const wallet of this.wallets) {
|
||||
if (c++ === index) {
|
||||
txs = txs.concat(wallet.getTransactions());
|
||||
}
|
||||
}
|
||||
return txs;
|
||||
}
|
||||
|
||||
let txs: Transaction[] = [];
|
||||
for (const wallet of this.wallets.filter(w => includeWalletsWithHideTransactionsEnabled || !w.getHideTransactionsInWalletsList())) {
|
||||
const walletTransactions = wallet.getTransactions();
|
||||
const walletID = wallet.getID();
|
||||
for (const t of walletTransactions) {
|
||||
t.walletPreferredBalanceUnit = wallet.getPreferredBalanceUnit();
|
||||
t.walletID = walletID;
|
||||
}
|
||||
txs = txs.concat(walletTransactions);
|
||||
}
|
||||
|
||||
return txs
|
||||
.sort((a, b) => {
|
||||
const bTime = new Date(b.received!).getTime();
|
||||
const aTime = new Date(a.received!).getTime();
|
||||
return bTime - aTime;
|
||||
})
|
||||
.slice(0, limit);
|
||||
};
|
||||
|
||||
/**
|
||||
* Getter for a sum of all balances of all wallets
|
||||
*/
|
||||
getBalance = (): number => {
|
||||
let finalBalance = 0;
|
||||
for (const wal of this.wallets) {
|
||||
finalBalance += wal.getBalance();
|
||||
}
|
||||
return finalBalance;
|
||||
};
|
||||
|
||||
isAdvancedModeEnabled = async (): Promise<boolean> => {
|
||||
try {
|
||||
return !!(await AsyncStorage.getItem(AppStorage.ADVANCED_MODE_ENABLED));
|
||||
} catch (_) {}
|
||||
return false;
|
||||
};
|
||||
|
||||
setIsAdvancedModeEnabled = async (value: boolean) => {
|
||||
await AsyncStorage.setItem(AppStorage.ADVANCED_MODE_ENABLED, value ? '1' : '');
|
||||
};
|
||||
|
||||
isHandoffEnabled = async (): Promise<boolean> => {
|
||||
try {
|
||||
return !!(await AsyncStorage.getItem(AppStorage.HANDOFF_STORAGE_KEY));
|
||||
} catch (_) {}
|
||||
return false;
|
||||
};
|
||||
|
||||
setIsHandoffEnabled = async (value: boolean): Promise<void> => {
|
||||
await AsyncStorage.setItem(AppStorage.HANDOFF_STORAGE_KEY, value ? '1' : '');
|
||||
};
|
||||
|
||||
isDoNotTrackEnabled = async (): Promise<boolean> => {
|
||||
try {
|
||||
const keyExists = await AsyncStorage.getItem(AppStorage.DO_NOT_TRACK);
|
||||
if (keyExists !== null) {
|
||||
const doNotTrackValue = !!keyExists;
|
||||
if (doNotTrackValue) {
|
||||
await DefaultPreference.setName('group.io.bluewallet.bluewallet');
|
||||
await DefaultPreference.set(AppStorage.DO_NOT_TRACK, '1');
|
||||
AsyncStorage.removeItem(AppStorage.DO_NOT_TRACK);
|
||||
} else {
|
||||
return Boolean(await DefaultPreference.get(AppStorage.DO_NOT_TRACK));
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
const doNotTrackValue = await DefaultPreference.get(AppStorage.DO_NOT_TRACK);
|
||||
return doNotTrackValue === '1' || false;
|
||||
};
|
||||
|
||||
setDoNotTrack = async (value: boolean) => {
|
||||
await DefaultPreference.setName('group.io.bluewallet.bluewallet');
|
||||
if (value) {
|
||||
await DefaultPreference.set(AppStorage.DO_NOT_TRACK, '1');
|
||||
} else {
|
||||
await DefaultPreference.clear(AppStorage.DO_NOT_TRACK);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Simple async sleeper function
|
||||
*/
|
||||
sleep = (ms: number): Promise<void> => {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
};
|
||||
|
||||
purgeRealmKeyValueFile() {
|
||||
const path = 'keyvalue.realm';
|
||||
return Realm.deleteFile({
|
||||
path,
|
||||
});
|
||||
}
|
||||
|
||||
async moveRealmFilesToCacheDirectory() {
|
||||
const documentPath = RNFS.DocumentDirectoryPath; // Path to documentPath folder
|
||||
const cachePath = RNFS.CachesDirectoryPath; // Path to cachePath folder
|
||||
try {
|
||||
if (!(await RNFS.exists(documentPath))) return; // If the documentPath directory does not exist, return (nothing to move)
|
||||
const files = await RNFS.readDir(documentPath); // Read all files in documentPath directory
|
||||
if (Array.isArray(files) && files.length === 0) return; // If there are no files, return (nothing to move)
|
||||
const appRealmFiles = files.filter(
|
||||
file => file.name.endsWith('.realm') || file.name.endsWith('.realm.lock') || file.name.includes('.realm.management'),
|
||||
);
|
||||
|
||||
for (const file of appRealmFiles) {
|
||||
const filePath = `${documentPath}/${file.name}`;
|
||||
const newFilePath = `${cachePath}/${file.name}`;
|
||||
const fileExists = await RNFS.exists(filePath); // Check if the file exists
|
||||
const cacheFileExists = await RNFS.exists(newFilePath); // Check if the file already exists in the cache directory
|
||||
|
||||
if (fileExists) {
|
||||
if (cacheFileExists) {
|
||||
await RNFS.unlink(newFilePath); // Delete the file in the cache directory if it exists
|
||||
console.log(`Existing file removed from cache: ${newFilePath}`);
|
||||
}
|
||||
await RNFS.moveFile(filePath, newFilePath); // Move the file
|
||||
console.log(`Moved Realm file: ${filePath} to ${newFilePath}`);
|
||||
} else {
|
||||
console.log(`File does not exist: ${filePath}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error moving Realm files:', error);
|
||||
throw new Error(`Error moving Realm files: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,11 +5,10 @@ import URL from 'url';
|
||||
|
||||
import { readFileOutsideSandbox } from '../blue_modules/fs';
|
||||
import { Chain } from '../models/bitcoinUnits';
|
||||
import { LightningCustodianWallet, WatchOnlyWallet } from './';
|
||||
import { AppStorage, LightningCustodianWallet, WatchOnlyWallet } from './';
|
||||
import Azteco from './azteco';
|
||||
import Lnurl from './lnurl';
|
||||
import type { TWallet } from './wallets/types';
|
||||
import { AppStorage } from '../BlueApp';
|
||||
|
||||
type TCompletionHandlerParams = [string, object];
|
||||
type TContext = {
|
||||
|
@ -18,3 +18,4 @@ export * from './wallets/multisig-hd-wallet';
|
||||
export * from './wallets/slip39-wallets';
|
||||
export * from './hd-segwit-bech32-transaction';
|
||||
export * from './multisig-cosigner';
|
||||
export * from './app-storage';
|
@ -12,7 +12,7 @@ import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
|
||||
import presentAlert from '../../components/Alert';
|
||||
import { requestCameraAuthorization } from '../../helpers/scan-qr';
|
||||
import { Button } from '../../components/Button';
|
||||
import { AppStorage } from '../../BlueApp';
|
||||
import { AppStorage } from '../../class';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
uri: {
|
||||
|
@ -18,7 +18,14 @@ import {
|
||||
import { BlueButtonLink, BlueFormLabel, BlueSpacing20, BlueSpacing40, BlueText } from '../../BlueComponents';
|
||||
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
|
||||
import { BlueStorageContext } from '../../blue_modules/storage-context';
|
||||
import { HDSegwitBech32Wallet, HDSegwitP2SHWallet, LightningCustodianWallet, LightningLdkWallet, SegwitP2SHWallet } from '../../class';
|
||||
import {
|
||||
AppStorage,
|
||||
HDSegwitBech32Wallet,
|
||||
HDSegwitP2SHWallet,
|
||||
LightningCustodianWallet,
|
||||
LightningLdkWallet,
|
||||
SegwitP2SHWallet,
|
||||
} from '../../class';
|
||||
import presentAlert from '../../components/Alert';
|
||||
import Button from '../../components/Button';
|
||||
import { LdkButton } from '../../components/LdkButton';
|
||||
@ -27,7 +34,6 @@ import { useTheme } from '../../components/themes';
|
||||
import useAsyncPromise from '../../hooks/useAsyncPromise';
|
||||
import loc from '../../loc';
|
||||
import { Chain } from '../../models/bitcoinUnits';
|
||||
import { AppStorage } from '../../BlueApp';
|
||||
import WalletButton from '../../components/WalletButton';
|
||||
import A from '../../blue_modules/analytics';
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
import assert from 'assert';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { SegwitP2SHWallet } from '../../class';
|
||||
import { AppStorage } from '../../BlueApp';
|
||||
import { SegwitP2SHWallet, AppStorage } from '../../class';
|
||||
|
||||
jest.mock('../../blue_modules/BlueElectrum', () => {
|
||||
return {
|
||||
|
Loading…
Reference in New Issue
Block a user