2020-07-20 16:58:35 +02:00
import Biometric from './class/biometrics' ;
import { Platform } from 'react-native' ;
2020-07-20 15:38:46 +02:00
import loc from './loc' ;
2023-04-21 17:39:12 +02:00
import AsyncStorage from '@react-native-async-storage/async-storage' ;
import RNSecureKeyStore , { ACCESSIBLE } from 'react-native-secure-key-store' ;
import * as Keychain from 'react-native-keychain' ;
import {
HDLegacyBreadwalletWallet ,
HDSegwitP2SHWallet ,
HDLegacyP2PKHWallet ,
WatchOnlyWallet ,
LegacyWallet ,
SegwitP2SHWallet ,
SegwitBech32Wallet ,
HDSegwitBech32Wallet ,
LightningCustodianWallet ,
HDLegacyElectrumSeedP2PKHWallet ,
HDSegwitElectrumSeedP2WPKHWallet ,
HDAezeedWallet ,
MultisigHDWallet ,
LightningLdkWallet ,
SLIP39SegwitP2SHWallet ,
SLIP39LegacyP2PKHWallet ,
SLIP39SegwitBech32Wallet ,
} from './class/' ;
import { randomBytes } from './class/rng' ;
import alert from './components/Alert' ;
const encryption = require ( './blue_modules/encryption' ) ;
const Realm = require ( 'realm' ) ;
const createHash = require ( 'create-hash' ) ;
let usedBucketNum = false ;
let savingInProgress = 0 ; // its both a flag and a counter of attempts to write to disk
2022-09-05 20:34:02 +02:00
const prompt = require ( './helpers/prompt' ) ;
2020-06-09 16:08:18 +02:00
const currency = require ( './blue_modules/currency' ) ;
2023-07-25 15:50:04 +02:00
const BlueElectrum = require ( './blue_modules/BlueElectrum' ) ;
2021-08-10 21:29:56 +02:00
BlueElectrum . connectMain ( ) ;
2023-04-21 17:39:12 +02:00
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 ] ;
constructor ( ) {
/** {Array.<AbstractWallet>} */
this . wallets = [ ] ;
this . tx _metadata = { } ;
this . cachedPassword = false ;
}
async migrateKeys ( ) {
if ( ! ( typeof navigator !== 'undefined' && navigator . product === 'ReactNative' ) ) return ;
for ( const key of this . constructor . 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
*
* @ 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 _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
*
* @ param key
* @ returns { Promise < any > | * }
* /
getItem = key => {
if ( typeof navigator !== 'undefined' && navigator . product === 'ReactNative' ) {
return RNSecureKeyStore . get ( key ) ;
} else {
return AsyncStorage . getItem ( key ) ;
}
} ;
/ * *
* @ throws Error
* @ param key { string }
* @ returns { Promise < * > | null }
* /
getItemWithFallbackToRealm = async key => {
let value ;
try {
return await this . getItem ( key ) ;
} catch ( error ) {
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 ) {
console . warn ( 'successfully recovered' , value . length , 'bytes from realm for key' , key ) ;
return value ;
}
return null ;
}
} ;
storageIsEncrypted = async ( ) => {
let data ;
try {
data = await this . getItemWithFallbackToRealm ( AppStorage . FLAG _ENCRYPTED ) ;
} catch ( error ) {
console . warn ( 'error reading `' + AppStorage . FLAG _ENCRYPTED + '` key:' , error . message ) ;
return false ;
}
return ! ! data ;
} ;
isPasswordInUse = async password => {
try {
let data = await this . getItem ( 'data' ) ;
data = this . decryptData ( data , password ) ;
return ! ! data ;
} catch ( _e ) {
return false ;
}
} ;
/ * *
* Iterates through all values of ` data ` trying to
* decrypt each one , and returns first one successfully decrypted
*
* @ param data { string } Serialized array
* @ param password
* @ returns { boolean | string } Either STRING of storage data ( which is stringified JSON ) or FALSE , which means failure
* /
decryptData ( data , password ) {
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 => {
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 => {
// 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
*
* @ returns { Promise . < boolean > } Success or failure
* /
createFakeStorage = async fakePassword => {
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 => {
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 .
*
* @ returns { Promise < Realm > }
* /
async getRealm ( ) {
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 path = this . hashIt ( this . hashIt ( password ) ) + '-wallettransactions.realm' ;
const schema = [
{
name : 'WalletTransactions' ,
properties : {
walletid : { type : 'string' , indexed : true } ,
internal : 'bool?' , // true - internal, false - external
index : 'int?' ,
tx : 'string' , // stringified json
} ,
} ,
] ;
return Realm . open ( {
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 ( ) {
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 = 'keyvalue.realm' ;
const schema = [
{
name : 'KeyValue' ,
primaryKey : 'key' ,
properties : {
key : { type : 'string' , indexed : true } ,
value : 'string' , // stringified json, or whatever
} ,
} ,
] ;
return Realm . open ( {
schema ,
path ,
encryptionKey ,
} ) ;
}
saveToRealmKeyValue ( realmkeyValue , key , value ) {
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 ) {
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 ) {
alert ( 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 ;
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 ( ) ;
if ( unserializedWallet . isHd ( ) && ! unserializedWallet . isXpubValid ( ) ) {
continue ;
}
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 HDLegacyElectrumSeedP2PKHWallet . type :
unserializedWallet = HDLegacyElectrumSeedP2PKHWallet . fromJson ( key ) ;
break ;
case HDSegwitElectrumSeedP2WPKHWallet . type :
unserializedWallet = HDSegwitElectrumSeedP2WPKHWallet . fromJson ( key ) ;
break ;
case MultisigHDWallet . type :
unserializedWallet = MultisigHDWallet . fromJson ( key ) ;
break ;
case HDAezeedWallet . type :
unserializedWallet = HDAezeedWallet . fromJson ( key ) ;
// 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 ) ;
break ;
case SLIP39SegwitP2SHWallet . type :
unserializedWallet = SLIP39SegwitP2SHWallet . fromJson ( key ) ;
break ;
case SLIP39LegacyP2PKHWallet . type :
unserializedWallet = SLIP39LegacyP2PKHWallet . fromJson ( key ) ;
break ;
case SLIP39SegwitBech32Wallet . type :
unserializedWallet = SLIP39SegwitBech32Wallet . fromJson ( key ) ;
break ;
case LightningCustodianWallet . type : {
/** @type {LightningCustodianWallet} */
unserializedWallet = LightningCustodianWallet . fromJson ( key ) ;
let lndhub = false ;
try {
lndhub = await AsyncStorage . getItem ( AppStorage . LNDHUB ) ;
2023-07-25 15:50:04 +02:00
} catch ( error ) {
console . warn ( error ) ;
2023-04-21 17:39:12 +02:00
}
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 ) ;
break ;
}
try {
if ( realm ) this . inflateWalletFromRealm ( realm , unserializedWallet ) ;
} catch ( error ) {
alert ( 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 => {
const ID = wallet . getID ( ) ;
const tempWallets = [ ] ;
if ( wallet . type === LightningLdkWallet . type ) {
/** @type {LightningLdkWallet} */
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 , walletToInflate ) {
const transactions = realm . objects ( 'WalletTransactions' ) ;
const transactionsForWallet = transactions . filtered ( ` walletid = " ${ walletToInflate . getID ( ) } " ` ) ;
for ( const tx of transactionsForWallet ) {
if ( tx . internal === false ) {
if ( walletToInflate . _hdWalletInstance ) {
walletToInflate . _hdWalletInstance . _txs _by _external _index [ tx . index ] =
walletToInflate . _hdWalletInstance . _txs _by _external _index [ tx . index ] || [ ] ;
walletToInflate . _hdWalletInstance . _txs _by _external _index [ tx . index ] . push ( JSON . parse ( tx . tx ) ) ;
} else {
walletToInflate . _txs _by _external _index [ tx . index ] = walletToInflate . _txs _by _external _index [ tx . index ] || [ ] ;
walletToInflate . _txs _by _external _index [ tx . index ] . push ( JSON . parse ( tx . tx ) ) ;
}
} else if ( tx . internal === true ) {
if ( walletToInflate . _hdWalletInstance ) {
walletToInflate . _hdWalletInstance . _txs _by _internal _index [ tx . index ] =
walletToInflate . _hdWalletInstance . _txs _by _internal _index [ tx . index ] || [ ] ;
walletToInflate . _hdWalletInstance . _txs _by _internal _index [ tx . index ] . push ( JSON . parse ( tx . tx ) ) ;
} else {
walletToInflate . _txs _by _internal _index [ tx . index ] = walletToInflate . _txs _by _internal _index [ tx . index ] || [ ] ;
walletToInflate . _txs _by _internal _index [ tx . index ] . push ( JSON . parse ( tx . tx ) ) ;
}
} else {
if ( ! Array . isArray ( walletToInflate . _txs _by _external _index ) ) walletToInflate . _txs _by _external _index = [ ] ;
walletToInflate . _txs _by _external _index = walletToInflate . _txs _by _external _index || [ ] ;
walletToInflate . _txs _by _external _index . push ( JSON . parse ( tx . tx ) ) ;
}
}
}
offloadWalletToRealm ( realm , wallet ) {
const id = wallet . getID ( ) ;
const walletToSave = 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 ) ;
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 ) ) {
const txs = walletToSave . _txs _by _external _index [ index ] ;
for ( const tx of txs ) {
realm . create (
'WalletTransactions' ,
{
walletid : id ,
internal : false ,
2023-07-25 15:50:04 +02:00
index : parseInt ( index , 10 ) ,
2023-04-21 17:39:12 +02:00
tx : JSON . stringify ( tx ) ,
} ,
Realm . UpdateMode . Modified ,
) ;
}
}
for ( const index of Object . keys ( walletToSave . _txs _by _internal _index ) ) {
const txs = walletToSave . _txs _by _internal _index [ index ] ;
for ( const tx of txs ) {
realm . create (
'WalletTransactions' ,
{
walletid : id ,
internal : true ,
2023-07-25 15:50:04 +02:00
index : parseInt ( index , 10 ) ,
2023-04-21 17:39:12 +02:00
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 ( ) {
if ( savingInProgress ) {
console . warn ( 'saveToDisk is in progress' ) ;
if ( ++ savingInProgress > 10 ) alert ( '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 ) {
alert ( error . message ) ;
}
for ( const key of this . wallets ) {
if ( typeof key === 'boolean' ) continue ;
key . prepareForSerialization ( ) ;
delete key . current ;
const keyCloned = Object . assign ( { } , key ) ; // stripped-down version of a wallet to save to secure keystore
if ( key . _hdWalletInstance ) keyCloned . _hdWalletInstance = Object . assign ( { } , key . _hdWalletInstance ) ;
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 ( key . _hdWalletInstance ) {
keyCloned . _hdWalletInstance . _txs _by _external _index = { } ;
keyCloned . _hdWalletInstance . _txs _by _internal _index = { } ;
}
if ( keyCloned . _bip47 _instance ) {
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 ) ) ;
}
}
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 ) {
console . error ( 'save to disk exception:' , error . message ) ;
alert ( '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
*
* @ return { Promise . < void > }
* /
fetchWalletBalances = async index => {
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 => {
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 ( wallet . fetchPendingTransactions ) {
await wallet . fetchPendingTransactions ( ) ;
}
if ( wallet . fetchUserInvoices ) {
await wallet . fetchUserInvoices ( ) ;
}
}
}
} else {
for ( const wallet of this . wallets ) {
await wallet . fetchTransactions ( ) ;
if ( wallet . fetchPendingTransactions ) {
await wallet . fetchPendingTransactions ( ) ;
}
if ( wallet . fetchUserInvoices ) {
await wallet . fetchUserInvoices ( ) ;
}
}
}
} ;
fetchSenderPaymentCodes = async index => {
console . log ( 'fetchSenderPaymentCodes for wallet#' , typeof index === 'undefined' ? '(all)' : index ) ;
if ( index || index === 0 ) {
try {
if ( ! ( this . wallets [ index ] . allowBIP47 ( ) && this . wallets [ index ] . isBIP47Enabled ( ) ) ) return ;
await this . wallets [ index ] . 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 ( ) ) ) continue ;
await wallet . fetchBIP47SenderPaymentCodes ( ) ;
} catch ( error ) {
console . error ( 'Failed to fetch sender payment codes for wallet' , wallet . label , error ) ;
}
}
}
} ;
/ * *
*
* @ 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 .
* @ param includeWalletsWithHideTransactionsEnabled { Boolean } Wallets ' _hideTransactionsInWalletsList property determines wether the user wants this wallet' s txs hidden from the main list view .
* @ return { Array }
* /
getTransactions = ( index , limit = Infinity , includeWalletsWithHideTransactionsEnabled = false ) => {
if ( index || index === 0 ) {
let txs = [ ] ;
let c = 0 ;
for ( const wallet of this . wallets ) {
if ( c ++ === index ) {
txs = txs . concat ( wallet . getTransactions ( ) ) ;
}
}
return txs ;
}
let txs = [ ] ;
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 ) ;
}
for ( const 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 ( const wal of this . wallets ) {
finalBalance += wal . getBalance ( ) ;
}
return finalBalance ;
} ;
isAdvancedModeEnabled = async ( ) => {
try {
return ! ! ( await AsyncStorage . getItem ( AppStorage . ADVANCED _MODE _ENABLED ) ) ;
} catch ( _ ) { }
return false ;
} ;
setIsAdvancedModeEnabled = async value => {
await AsyncStorage . setItem ( AppStorage . ADVANCED _MODE _ENABLED , value ? '1' : '' ) ;
} ;
isHandoffEnabled = async ( ) => {
try {
return ! ! ( await AsyncStorage . getItem ( AppStorage . HANDOFF _STORAGE _KEY ) ) ;
} catch ( _ ) { }
return false ;
} ;
setIsHandoffEnabled = async value => {
await AsyncStorage . setItem ( AppStorage . HANDOFF _STORAGE _KEY , value ? '1' : '' ) ;
} ;
isDoNotTrackEnabled = async ( ) => {
try {
return ! ! ( await AsyncStorage . getItem ( AppStorage . DO _NOT _TRACK ) ) ;
} catch ( _ ) { }
return false ;
} ;
setDoNotTrack = async value => {
await AsyncStorage . setItem ( AppStorage . DO _NOT _TRACK , value ? '1' : '' ) ;
} ;
/ * *
* Simple async sleeper function
*
* @ param ms { number } Milliseconds to sleep
* @ returns { Promise < Promise < * > | Promise < * >> }
* /
sleep = ms => {
return new Promise ( resolve => setTimeout ( resolve , ms ) ) ;
} ;
purgeRealmKeyValueFile ( ) {
const path = 'keyvalue.realm' ;
return Realm . deleteFile ( {
path ,
} ) ;
}
}
2021-07-17 22:37:06 +02:00
const BlueApp = new AppStorage ( ) ;
2020-07-20 16:58:35 +02:00
// If attempt reaches 10, a wipe keychain option will be provided to the user.
let unlockAttempt = 0 ;
2018-03-31 02:03:58 +02:00
2020-10-24 19:20:59 +02:00
const startAndDecrypt = async retry => {
2018-12-31 17:20:49 +01:00
console . log ( 'startAndDecrypt' ) ;
if ( BlueApp . getWallets ( ) . length > 0 ) {
console . log ( 'App already has some wallets, so we are in already started state, exiting startAndDecrypt' ) ;
2020-07-23 20:04:44 +02:00
return true ;
2018-12-31 17:20:49 +01:00
}
2021-05-18 22:38:18 +02:00
await BlueApp . migrateKeys ( ) ;
2018-03-31 02:03:58 +02:00
let password = false ;
if ( await BlueApp . storageIsEncrypted ( ) ) {
2018-04-01 01:16:42 +02:00
do {
2018-10-09 06:25:36 +02:00
password = await prompt ( ( retry && loc . _ . bad _password ) || loc . _ . enter _password , loc . _ . storage _is _encrypted , false ) ;
2018-04-01 01:16:42 +02:00
} while ( ! password ) ;
2018-03-31 02:03:58 +02:00
}
2021-02-13 00:38:29 +01:00
let success = false ;
let wasException = false ;
try {
success = await BlueApp . loadFromDisk ( password ) ;
} catch ( error ) {
// in case of exception reading from keystore, lets retry instead of assuming there is no storage and
// proceeding with no wallets
2021-05-24 14:16:03 +02:00
console . warn ( 'exception loading from disk:' , error ) ;
2021-02-13 00:38:29 +01:00
wasException = true ;
}
if ( wasException ) {
// retrying, but only once
2018-07-02 15:51:24 +02:00
try {
2021-02-13 00:38:29 +01:00
await new Promise ( resolve => setTimeout ( resolve , 3000 ) ) ; // sleep
success = await BlueApp . loadFromDisk ( password ) ;
2021-05-24 14:16:03 +02:00
} catch ( error ) {
console . warn ( 'second exception loading from disk:' , error ) ;
}
2021-02-13 00:38:29 +01:00
}
2018-07-02 15:51:24 +02:00
2021-02-13 00:38:29 +01:00
if ( success ) {
console . log ( 'loaded from disk' ) ;
2020-07-20 16:58:35 +02:00
// We want to return true to let the UnlockWith screen that its ok to proceed.
return true ;
2018-03-31 02:03:58 +02:00
}
2021-02-13 00:38:29 +01:00
if ( password ) {
2018-03-31 02:03:58 +02:00
// we had password and yet could not load/decrypt
2020-07-20 16:58:35 +02:00
unlockAttempt ++ ;
if ( unlockAttempt < 10 || Platform . OS !== 'ios' ) {
return startAndDecrypt ( true ) ;
} else {
unlockAttempt = 0 ;
Biometric . showKeychainWipeAlert ( ) ;
// We want to return false to let the UnlockWith screen that it is NOT ok to proceed.
return false ;
}
} else {
2020-07-21 00:14:02 +02:00
unlockAttempt = 0 ;
2020-07-20 16:58:35 +02:00
// Return true because there was no wallet data in keychain. Proceed.
return true ;
2018-03-31 02:03:58 +02:00
}
2020-10-24 19:20:59 +02:00
} ;
2018-03-31 15:43:08 +02:00
2018-12-11 23:52:46 +01:00
BlueApp . startAndDecrypt = startAndDecrypt ;
2023-04-21 17:39:12 +02:00
BlueApp . AppStorage = AppStorage ;
2021-09-09 21:36:35 +02:00
currency . init ( ) ;
2018-01-30 23:42:38 +01:00
2018-03-17 21:39:21 +01:00
module . exports = BlueApp ;