2023-04-21 16:39:12 +01:00
import AsyncStorage from '@react-native-async-storage/async-storage' ;
2024-03-15 23:05:15 +03:00
import createHash from 'create-hash' ;
import { Platform } from 'react-native' ;
import DefaultPreference from 'react-native-default-preference' ;
2023-04-21 16:39:12 +01:00
import * as Keychain from 'react-native-keychain' ;
2024-03-15 23:05:15 +03:00
import RNSecureKeyStore , { ACCESSIBLE } from 'react-native-secure-key-store' ;
import Realm from 'realm' ;
import BlueElectrum from './blue_modules/BlueElectrum' ;
import { initCurrencyDaemon } from './blue_modules/currency' ;
2023-04-21 16:39:12 +01:00
import {
2024-03-15 23:05:15 +03:00
HDAezeedWallet ,
2023-04-21 16:39:12 +01:00
HDLegacyBreadwalletWallet ,
2024-03-15 23:05:15 +03:00
HDLegacyElectrumSeedP2PKHWallet ,
2023-04-21 16:39:12 +01:00
HDLegacyP2PKHWallet ,
HDSegwitBech32Wallet ,
HDSegwitElectrumSeedP2WPKHWallet ,
2024-03-15 23:05:15 +03:00
HDSegwitP2SHWallet ,
LegacyWallet ,
LightningCustodianWallet ,
2023-04-21 16:39:12 +01:00
LightningLdkWallet ,
2024-03-15 23:05:15 +03:00
MultisigHDWallet ,
2023-04-21 16:39:12 +01:00
SLIP39LegacyP2PKHWallet ,
SLIP39SegwitBech32Wallet ,
2024-03-15 23:05:15 +03:00
SLIP39SegwitP2SHWallet ,
SegwitBech32Wallet ,
SegwitP2SHWallet ,
WatchOnlyWallet ,
2023-04-21 16:39:12 +01:00
} from './class/' ;
2024-03-15 23:05:15 +03:00
import Biometric from './class/biometrics' ;
2023-04-21 16:39:12 +01:00
import { randomBytes } from './class/rng' ;
2024-03-15 23:05:15 +03:00
import { TWallet , Transaction } from './class/wallets/types' ;
2024-02-07 15:24:24 -04:00
import presentAlert from './components/Alert' ;
2024-03-15 23:05:15 +03:00
import loc from './loc' ;
2023-04-21 16:39:12 +01:00
2024-03-15 23:05:15 +03:00
const prompt = require ( './helpers/prompt' ) ;
2023-04-21 16:39:12 +01:00
const encryption = require ( './blue_modules/encryption' ) ;
2024-03-15 23:05:15 +03:00
let usedBucketNum : boolean | number = false ;
2023-04-21 16:39:12 +01:00
let savingInProgress = 0 ; // its both a flag and a counter of attempts to write to disk
2024-03-15 23:05:15 +03:00
BlueElectrum . connectMain ( ) ;
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' ;
2023-04-21 16:39:12 +01:00
2024-03-15 23:05:15 +03:00
export class AppStorage {
2023-04-21 16:39:12 +01:00
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 ] ;
2024-03-15 23:05:15 +03:00
public cachedPassword? : false | string ;
public tx_metadata : TTXMetadata ;
public wallets : TWallet [ ] ;
2023-04-21 16:39:12 +01:00
constructor ( ) {
this . wallets = [ ] ;
this . tx_metadata = { } ;
this . cachedPassword = false ;
}
async migrateKeys() {
2024-03-15 23:05:15 +03:00
// do not migrate keys if we are not in RN env
if ( ! isReactNative ) {
return ;
}
for ( const key of AppStorage . keys2migrate ) {
2023-04-21 16:39:12 +01:00
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
* /
2024-03-15 23:05:15 +03:00
setItem = ( key : string , value : any ) : Promise < any > = > {
if ( isReactNative ) {
2023-04-21 16:39:12 +01:00
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
* /
2024-03-15 23:05:15 +03:00
getItem = ( key : string ) : Promise < any > = > {
if ( isReactNative ) {
2023-04-21 16:39:12 +01:00
return RNSecureKeyStore . get ( key ) ;
} else {
return AsyncStorage . getItem ( key ) ;
}
} ;
2024-03-15 23:05:15 +03:00
getItemWithFallbackToRealm = async ( key : string ) : Promise < any | null > = > {
2023-04-21 16:39:12 +01:00
let value ;
try {
return await this . getItem ( key ) ;
2024-03-15 23:05:15 +03:00
} catch ( error : any ) {
2023-04-21 16:39:12 +01:00
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 ) {
2024-03-15 23:05:15 +03:00
// @ts-ignore value.length
2023-04-21 16:39:12 +01:00
console . warn ( 'successfully recovered' , value . length , 'bytes from realm for key' , key ) ;
return value ;
}
return null ;
}
} ;
2024-03-15 23:05:15 +03:00
storageIsEncrypted = async ( ) : Promise < boolean > = > {
2023-04-21 16:39:12 +01:00
let data ;
try {
data = await this . getItemWithFallbackToRealm ( AppStorage . FLAG_ENCRYPTED ) ;
2024-03-15 23:05:15 +03:00
} catch ( error : any ) {
2023-04-21 16:39:12 +01:00
console . warn ( 'error reading `' + AppStorage . FLAG_ENCRYPTED + '` key:' , error . message ) ;
return false ;
}
2024-03-15 23:05:15 +03:00
return Boolean ( data ) ;
2023-04-21 16:39:12 +01:00
} ;
2024-03-15 23:05:15 +03:00
isPasswordInUse = async ( password : string ) = > {
2023-04-21 16:39:12 +01:00
try {
let data = await this . getItem ( 'data' ) ;
data = this . decryptData ( data , password ) ;
2024-03-15 23:05:15 +03:00
return Boolean ( data ) ;
2023-04-21 16:39:12 +01:00
} catch ( _e ) {
return false ;
}
} ;
/ * *
* Iterates through all values of ` data ` trying to
* decrypt each one , and returns first one successfully decrypted
* /
2024-03-15 23:05:15 +03:00
decryptData ( data : string , password : string ) : boolean | string {
2023-04-21 16:39:12 +01:00
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 ;
}
2024-03-15 23:05:15 +03:00
decryptStorage = async ( password : string ) : Promise < boolean > = > {
2023-04-21 16:39:12 +01:00
if ( password === this . cachedPassword ) {
2024-03-20 21:20:43 +03:00
this . cachedPassword = undefined ;
2023-04-21 16:39:12 +01:00
await this . saveToDisk ( ) ;
this . wallets = [ ] ;
2024-03-15 23:05:15 +03:00
this . tx_metadata = { } ;
2023-04-21 16:39:12 +01:00
return this . loadFromDisk ( ) ;
} else {
throw new Error ( 'Incorrect password. Please, try again.' ) ;
}
} ;
2024-03-15 23:05:15 +03:00
encryptStorage = async ( password : string ) : Promise < void > = > {
2023-04-21 16:39:12 +01:00
// 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
* /
2024-03-15 23:05:15 +03:00
createFakeStorage = async ( fakePassword : string ) : Promise < boolean > = > {
2023-04-21 16:39:12 +01:00
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 ;
} ;
2024-03-15 23:05:15 +03:00
hashIt = ( s : string ) : string = > {
2023-04-21 16:39:12 +01:00
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 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
} ,
} ,
] ;
2024-03-15 23:05:15 +03:00
// @ts-ignore schema doesn't match Realm's schema type
2023-04-21 16:39:12 +01:00
return Realm . open ( {
2024-03-15 23:05:15 +03:00
// @ts-ignore schema doesn't match Realm's schema type
2023-04-21 16:39:12 +01:00
schema ,
path ,
encryptionKey ,
} ) ;
}
/ * *
* Returns instace of the Realm database , which is encrypted by device unique id
* Database file is static .
*
* @returns { Promise < Realm > }
* /
2024-03-15 23:05:15 +03:00
async openRealmKeyValue ( ) : Promise < Realm > {
2023-04-21 16:39:12 +01:00
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
} ,
} ,
] ;
2024-03-15 23:05:15 +03:00
// @ts-ignore schema doesn't match Realm's schema type
2023-04-21 16:39:12 +01:00
return Realm . open ( {
2024-03-15 23:05:15 +03:00
// @ts-ignore schema doesn't match Realm's schema type
2023-04-21 16:39:12 +01:00
schema ,
path ,
encryptionKey ,
} ) ;
}
2024-03-15 23:05:15 +03:00
saveToRealmKeyValue ( realmkeyValue : Realm , key : string , value : any ) {
2023-04-21 16:39:12 +01:00
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 > }
* /
2024-03-15 23:05:15 +03:00
async loadFromDisk ( password? : string ) : Promise < boolean > {
2023-04-21 16:39:12 +01:00
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 ( ) ;
2024-03-15 23:05:15 +03:00
} catch ( error : any ) {
2024-02-07 15:24:24 -04:00
presentAlert ( { message : error.message } ) ;
2023-04-21 16:39:12 +01:00
}
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 ) ;
2024-03-15 23:05:15 +03:00
let unserializedWallet : TWallet ;
2023-04-21 16:39:12 +01:00
switch ( tempObj . type ) {
case SegwitBech32Wallet . type :
2024-03-15 23:05:15 +03:00
unserializedWallet = SegwitBech32Wallet . fromJson ( key ) as unknown as SegwitBech32Wallet ;
2023-04-21 16:39:12 +01:00
break ;
case SegwitP2SHWallet . type :
2024-03-15 23:05:15 +03:00
unserializedWallet = SegwitP2SHWallet . fromJson ( key ) as unknown as SegwitP2SHWallet ;
2023-04-21 16:39:12 +01:00
break ;
case WatchOnlyWallet . type :
2024-03-15 23:05:15 +03:00
unserializedWallet = WatchOnlyWallet . fromJson ( key ) as unknown as WatchOnlyWallet ;
2023-04-21 16:39:12 +01:00
unserializedWallet . init ( ) ;
if ( unserializedWallet . isHd ( ) && ! unserializedWallet . isXpubValid ( ) ) {
continue ;
}
break ;
case HDLegacyP2PKHWallet . type :
2024-03-15 23:05:15 +03:00
unserializedWallet = HDLegacyP2PKHWallet . fromJson ( key ) as unknown as HDLegacyP2PKHWallet ;
2023-04-21 16:39:12 +01:00
break ;
case HDSegwitP2SHWallet . type :
2024-03-15 23:05:15 +03:00
unserializedWallet = HDSegwitP2SHWallet . fromJson ( key ) as unknown as HDSegwitP2SHWallet ;
2023-04-21 16:39:12 +01:00
break ;
case HDSegwitBech32Wallet . type :
2024-03-15 23:05:15 +03:00
unserializedWallet = HDSegwitBech32Wallet . fromJson ( key ) as unknown as HDSegwitBech32Wallet ;
2023-04-21 16:39:12 +01:00
break ;
case HDLegacyBreadwalletWallet . type :
2024-03-15 23:05:15 +03:00
unserializedWallet = HDLegacyBreadwalletWallet . fromJson ( key ) as unknown as HDLegacyBreadwalletWallet ;
2023-04-21 16:39:12 +01:00
break ;
case HDLegacyElectrumSeedP2PKHWallet . type :
2024-03-15 23:05:15 +03:00
unserializedWallet = HDLegacyElectrumSeedP2PKHWallet . fromJson ( key ) as unknown as HDLegacyElectrumSeedP2PKHWallet ;
2023-04-21 16:39:12 +01:00
break ;
case HDSegwitElectrumSeedP2WPKHWallet . type :
2024-03-15 23:05:15 +03:00
unserializedWallet = HDSegwitElectrumSeedP2WPKHWallet . fromJson ( key ) as unknown as HDSegwitElectrumSeedP2WPKHWallet ;
2023-04-21 16:39:12 +01:00
break ;
case MultisigHDWallet . type :
2024-03-15 23:05:15 +03:00
unserializedWallet = MultisigHDWallet . fromJson ( key ) as unknown as MultisigHDWallet ;
2023-04-21 16:39:12 +01:00
break ;
case HDAezeedWallet . type :
2024-03-15 23:05:15 +03:00
unserializedWallet = HDAezeedWallet . fromJson ( key ) as unknown as HDAezeedWallet ;
2023-04-21 16:39:12 +01:00
// 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 :
2024-03-15 23:05:15 +03:00
unserializedWallet = LightningLdkWallet . fromJson ( key ) as unknown as LightningLdkWallet ;
2023-04-21 16:39:12 +01:00
break ;
case SLIP39SegwitP2SHWallet . type :
2024-03-15 23:05:15 +03:00
unserializedWallet = SLIP39SegwitP2SHWallet . fromJson ( key ) as unknown as SLIP39SegwitP2SHWallet ;
2023-04-21 16:39:12 +01:00
break ;
case SLIP39LegacyP2PKHWallet . type :
2024-03-15 23:05:15 +03:00
unserializedWallet = SLIP39LegacyP2PKHWallet . fromJson ( key ) as unknown as SLIP39LegacyP2PKHWallet ;
2023-04-21 16:39:12 +01:00
break ;
case SLIP39SegwitBech32Wallet . type :
2024-03-15 23:05:15 +03:00
unserializedWallet = SLIP39SegwitBech32Wallet . fromJson ( key ) as unknown as SLIP39SegwitBech32Wallet ;
2023-04-21 16:39:12 +01:00
break ;
case LightningCustodianWallet . type : {
2024-03-15 23:05:15 +03:00
unserializedWallet = LightningCustodianWallet . fromJson ( key ) as unknown as LightningCustodianWallet ;
let lndhub : false | any = false ;
2023-04-21 16:39:12 +01:00
try {
lndhub = await AsyncStorage . getItem ( AppStorage . LNDHUB ) ;
2023-07-25 14:50:04 +01:00
} catch ( error ) {
console . warn ( error ) ;
2023-04-21 16:39:12 +01: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 :
2024-03-15 23:05:15 +03:00
unserializedWallet = LegacyWallet . fromJson ( key ) as unknown as LegacyWallet ;
2023-04-21 16:39:12 +01:00
break ;
}
try {
if ( realm ) this . inflateWalletFromRealm ( realm , unserializedWallet ) ;
2024-03-15 23:05:15 +03:00
} catch ( error : any ) {
2024-02-07 15:24:24 -04:00
presentAlert ( { message : error.message } ) ;
2023-04-21 16:39:12 +01:00
}
// 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 }
* /
2024-03-15 23:05:15 +03:00
deleteWallet = ( wallet : TWallet ) : void = > {
2023-04-21 16:39:12 +01:00
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 ;
} ;
2024-03-15 23:05:15 +03:00
inflateWalletFromRealm ( realm : Realm , walletToInflate : TWallet ) {
2023-04-21 16:39:12 +01:00
const transactions = realm . objects ( 'WalletTransactions' ) ;
2024-03-15 23:05:15 +03:00
const transactionsForWallet = transactions . filtered ( ` walletid = " ${ walletToInflate . getID ( ) } " ` ) as unknown as TRealmTransaction [ ] ;
2023-04-21 16:39:12 +01:00
for ( const tx of transactionsForWallet ) {
if ( tx . internal === false ) {
2024-03-15 23:05:15 +03:00
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 ) ;
2023-04-21 16:39:12 +01:00
} else {
walletToInflate . _txs_by_external_index [ tx . index ] = walletToInflate . _txs_by_external_index [ tx . index ] || [ ] ;
2024-03-15 23:05:15 +03:00
const transaction = JSON . parse ( tx . tx ) ;
( walletToInflate . _txs_by_external_index [ tx . index ] as Transaction [ ] ) . push ( transaction ) ;
2023-04-21 16:39:12 +01:00
}
} else if ( tx . internal === true ) {
2024-03-15 23:05:15 +03:00
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 ) ;
2023-04-21 16:39:12 +01:00
} else {
walletToInflate . _txs_by_internal_index [ tx . index ] = walletToInflate . _txs_by_internal_index [ tx . index ] || [ ] ;
2024-03-15 23:05:15 +03:00
const transaction = JSON . parse ( tx . tx ) ;
( walletToInflate . _txs_by_internal_index [ tx . index ] as Transaction [ ] ) . push ( transaction ) ;
2023-04-21 16:39:12 +01:00
}
} else {
if ( ! Array . isArray ( walletToInflate . _txs_by_external_index ) ) walletToInflate . _txs_by_external_index = [ ] ;
walletToInflate . _txs_by_external_index = walletToInflate . _txs_by_external_index || [ ] ;
2024-03-15 23:05:15 +03:00
const transaction = JSON . parse ( tx . tx ) ;
( walletToInflate . _txs_by_external_index as Transaction [ ] ) . push ( transaction ) ;
2023-04-21 16:39:12 +01:00
}
}
}
2024-03-15 23:05:15 +03:00
offloadWalletToRealm ( realm : Realm , wallet : TWallet ) : void {
2023-04-21 16:39:12 +01:00
const id = wallet . getID ( ) ;
2024-03-15 23:05:15 +03:00
const walletToSave = ( '_hdWalletInstance' in wallet && wallet . _hdWalletInstance ) || wallet ;
2023-04-21 16:39:12 +01:00
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 ) ;
2024-03-15 23:05:15 +03:00
// @ts-ignore walletToSave._txs_by_external_index is array
2023-04-21 16:39:12 +01:00
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 ) ) {
2024-03-15 23:05:15 +03:00
// @ts-ignore index is number
2023-04-21 16:39:12 +01:00
const txs = walletToSave . _txs_by_external_index [ index ] ;
for ( const tx of txs ) {
realm . create (
'WalletTransactions' ,
{
walletid : id ,
internal : false ,
2023-07-25 14:50:04 +01:00
index : parseInt ( index , 10 ) ,
2023-04-21 16:39:12 +01:00
tx : JSON.stringify ( tx ) ,
} ,
Realm . UpdateMode . Modified ,
) ;
}
}
for ( const index of Object . keys ( walletToSave . _txs_by_internal_index ) ) {
2024-03-15 23:05:15 +03:00
// @ts-ignore index is number
2023-04-21 16:39:12 +01:00
const txs = walletToSave . _txs_by_internal_index [ index ] ;
for ( const tx of txs ) {
realm . create (
'WalletTransactions' ,
{
walletid : id ,
internal : true ,
2023-07-25 14:50:04 +01:00
index : parseInt ( index , 10 ) ,
2023-04-21 16:39:12 +01: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
* /
2024-03-15 23:05:15 +03:00
async saveToDisk ( ) : Promise < void > {
2023-04-21 16:39:12 +01:00
if ( savingInProgress ) {
console . warn ( 'saveToDisk is in progress' ) ;
2024-02-07 15:24:24 -04:00
if ( ++ savingInProgress > 10 ) presentAlert ( { message : 'Critical error. Last actions were not saved' } ) ; // should never happen
2023-04-21 16:39:12 +01:00
await new Promise ( resolve = > setTimeout ( resolve , 1000 * savingInProgress ) ) ; // sleep
return this . saveToDisk ( ) ;
}
savingInProgress = 1 ;
try {
const walletsToSave = [ ] ;
let realm ;
try {
realm = await this . getRealm ( ) ;
2024-03-15 23:05:15 +03:00
} catch ( error : any ) {
2024-02-07 15:24:24 -04:00
presentAlert ( { message : error.message } ) ;
2023-04-21 16:39:12 +01:00
}
for ( const key of this . wallets ) {
if ( typeof key === 'boolean' ) continue ;
key . prepareForSerialization ( ) ;
2024-03-15 23:05:15 +03:00
// @ts-ignore wtf is wallet.current? Does it even exist?
2023-04-21 16:39:12 +01:00
delete key . current ;
const keyCloned = Object . assign ( { } , key ) ; // stripped-down version of a wallet to save to secure keystore
2024-03-15 23:05:15 +03:00
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 = { } ;
}
2023-04-21 16:39:12 +01:00
if ( realm ) this . offloadWalletToRealm ( realm , key ) ;
// stripping down:
if ( key . _txs_by_external_index ) {
keyCloned . _txs_by_external_index = { } ;
keyCloned . _txs_by_internal_index = { } ;
}
2024-03-15 23:05:15 +03:00
if ( '_bip47_instance' in keyCloned ) {
2023-04-21 16:39:12 +01:00
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 ) ) ;
}
}
2024-03-15 23:05:15 +03:00
// @ts-ignore bla bla bla
2023-04-21 16:39:12 +01:00
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 ( ) ;
2024-03-15 23:05:15 +03:00
} catch ( error : any ) {
2023-04-21 16:39:12 +01:00
console . error ( 'save to disk exception:' , error . message ) ;
2024-02-07 15:24:24 -04:00
presentAlert ( { message : 'save to disk exception: ' + error . message } ) ;
2023-04-21 16:39:12 +01:00
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
* /
2024-03-15 23:05:15 +03:00
fetchWalletBalances = async ( index? : number ) : Promise < void > = > {
2023-04-21 16:39:12 +01:00
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 > }
* /
2024-03-15 23:05:15 +03:00
fetchWalletTransactions = async ( index? : number ) = > {
2023-04-21 16:39:12 +01:00
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 ( ) ;
2024-03-15 23:05:15 +03:00
if ( 'fetchPendingTransactions' in wallet ) {
2023-04-21 16:39:12 +01:00
await wallet . fetchPendingTransactions ( ) ;
await wallet . fetchUserInvoices ( ) ;
}
}
}
} else {
for ( const wallet of this . wallets ) {
await wallet . fetchTransactions ( ) ;
2024-03-15 23:05:15 +03:00
if ( 'fetchPendingTransactions' in wallet ) {
2023-04-21 16:39:12 +01:00
await wallet . fetchPendingTransactions ( ) ;
await wallet . fetchUserInvoices ( ) ;
}
}
}
} ;
2024-03-15 23:05:15 +03:00
fetchSenderPaymentCodes = async ( index? : number ) = > {
2023-04-21 16:39:12 +01:00
console . log ( 'fetchSenderPaymentCodes for wallet#' , typeof index === 'undefined' ? '(all)' : index ) ;
if ( index || index === 0 ) {
2024-03-15 23:05:15 +03:00
const wallet = this . wallets [ index ] ;
2023-04-21 16:39:12 +01:00
try {
2024-03-15 23:05:15 +03:00
if ( ! ( wallet . allowBIP47 ( ) && wallet . isBIP47Enabled ( ) && 'fetchBIP47SenderPaymentCodes' in wallet ) ) return ;
await wallet . fetchBIP47SenderPaymentCodes ( ) ;
2023-04-21 16:39:12 +01:00
} catch ( error ) {
console . error ( 'Failed to fetch sender payment codes for wallet' , index , error ) ;
}
} else {
for ( const wallet of this . wallets ) {
try {
2024-03-15 23:05:15 +03:00
if ( ! ( wallet . allowBIP47 ( ) && wallet . isBIP47Enabled ( ) && 'fetchBIP47SenderPaymentCodes' in wallet ) ) continue ;
2023-04-21 16:39:12 +01:00
await wallet . fetchBIP47SenderPaymentCodes ( ) ;
} catch ( error ) {
console . error ( 'Failed to fetch sender payment codes for wallet' , wallet . label , error ) ;
}
}
}
} ;
2024-03-15 23:05:15 +03:00
getWallets = ( ) : TWallet [ ] = > {
2023-04-21 16:39:12 +01:00
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 .
* /
2024-03-15 23:05:15 +03:00
getTransactions = (
index? : number ,
limit : number = Infinity ,
includeWalletsWithHideTransactionsEnabled : boolean = false ,
) : Transaction [ ] = > {
2023-04-21 16:39:12 +01:00
if ( index || index === 0 ) {
2024-03-15 23:05:15 +03:00
let txs : Transaction [ ] = [ ] ;
2023-04-21 16:39:12 +01:00
let c = 0 ;
for ( const wallet of this . wallets ) {
if ( c ++ === index ) {
txs = txs . concat ( wallet . getTransactions ( ) ) ;
}
}
return txs ;
}
2024-03-15 23:05:15 +03:00
let txs : Transaction [ ] = [ ] ;
2023-04-21 16:39:12 +01:00
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
2024-03-15 23:05:15 +03:00
. sort ( ( a , b ) = > {
const bTime = new Date ( b . received ! ) . getTime ( ) ;
const aTime = new Date ( a . received ! ) . getTime ( ) ;
return bTime - aTime ;
2023-04-21 16:39:12 +01:00
} )
. slice ( 0 , limit ) ;
} ;
/ * *
* Getter for a sum of all balances of all wallets
* /
2024-03-15 23:05:15 +03:00
getBalance = ( ) : number = > {
2023-04-21 16:39:12 +01:00
let finalBalance = 0 ;
for ( const wal of this . wallets ) {
finalBalance += wal . getBalance ( ) ;
}
return finalBalance ;
} ;
2024-03-15 23:05:15 +03:00
isAdvancedModeEnabled = async ( ) : Promise < boolean > = > {
2023-04-21 16:39:12 +01:00
try {
return ! ! ( await AsyncStorage . getItem ( AppStorage . ADVANCED_MODE_ENABLED ) ) ;
} catch ( _ ) { }
return false ;
} ;
2024-03-15 23:05:15 +03:00
setIsAdvancedModeEnabled = async ( value : boolean ) = > {
2023-04-21 16:39:12 +01:00
await AsyncStorage . setItem ( AppStorage . ADVANCED_MODE_ENABLED , value ? '1' : '' ) ;
} ;
2024-03-15 23:05:15 +03:00
isHandoffEnabled = async ( ) : Promise < boolean > = > {
2023-04-21 16:39:12 +01:00
try {
return ! ! ( await AsyncStorage . getItem ( AppStorage . HANDOFF_STORAGE_KEY ) ) ;
} catch ( _ ) { }
return false ;
} ;
2024-03-15 23:05:15 +03:00
setIsHandoffEnabled = async ( value : boolean ) : Promise < void > = > {
2023-04-21 16:39:12 +01:00
await AsyncStorage . setItem ( AppStorage . HANDOFF_STORAGE_KEY , value ? '1' : '' ) ;
} ;
2024-03-15 23:05:15 +03:00
isDoNotTrackEnabled = async ( ) : Promise < boolean > = > {
2023-04-21 16:39:12 +01:00
try {
return ! ! ( await AsyncStorage . getItem ( AppStorage . DO_NOT_TRACK ) ) ;
} catch ( _ ) { }
return false ;
} ;
2024-03-15 23:05:15 +03:00
setDoNotTrack = async ( value : string ) = > {
2023-04-21 16:39:12 +01:00
await AsyncStorage . setItem ( AppStorage . DO_NOT_TRACK , value ? '1' : '' ) ;
2024-01-28 23:04:48 -04:00
await DefaultPreference . setName ( 'group.io.bluewallet.bluewallet' ) ;
2024-03-15 23:05:15 +03:00
await DefaultPreference . set ( AppStorage . DO_NOT_TRACK , value ? '1' : '' ) ;
2023-04-21 16:39:12 +01:00
} ;
/ * *
* Simple async sleeper function
* /
2024-03-15 23:05:15 +03:00
sleep = ( ms : number ) : Promise < void > = > {
2023-04-21 16:39:12 +01:00
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 10:58:35 -04:00
// If attempt reaches 10, a wipe keychain option will be provided to the user.
let unlockAttempt = 0 ;
2018-03-31 01:03:58 +01:00
2024-03-15 23:05:15 +03:00
export const startAndDecrypt = async ( retry? : boolean ) : Promise < boolean > = > {
2018-12-31 16:20:49 +00: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 19:04:44 +01:00
return true ;
2018-12-31 16:20:49 +00:00
}
2021-05-18 21:38:18 +01:00
await BlueApp . migrateKeys ( ) ;
2024-03-15 23:05:15 +03:00
let password : undefined | string ;
2018-03-31 01:03:58 +01:00
if ( await BlueApp . storageIsEncrypted ( ) ) {
2018-04-01 00:16:42 +01:00
do {
2018-10-09 00:25:36 -04:00
password = await prompt ( ( retry && loc . _ . bad_password ) || loc . _ . enter_password , loc . _ . storage_is_encrypted , false ) ;
2018-04-01 00:16:42 +01:00
} while ( ! password ) ;
2018-03-31 01:03:58 +01:00
}
2021-02-12 23:38:29 +00: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 13:16:03 +01:00
console . warn ( 'exception loading from disk:' , error ) ;
2021-02-12 23:38:29 +00:00
wasException = true ;
}
if ( wasException ) {
// retrying, but only once
2018-07-02 14:51:24 +01:00
try {
2021-02-12 23:38:29 +00:00
await new Promise ( resolve = > setTimeout ( resolve , 3000 ) ) ; // sleep
success = await BlueApp . loadFromDisk ( password ) ;
2021-05-24 13:16:03 +01:00
} catch ( error ) {
console . warn ( 'second exception loading from disk:' , error ) ;
}
2021-02-12 23:38:29 +00:00
}
2018-07-02 14:51:24 +01:00
2021-02-12 23:38:29 +00:00
if ( success ) {
console . log ( 'loaded from disk' ) ;
2020-07-20 10:58:35 -04:00
// We want to return true to let the UnlockWith screen that its ok to proceed.
return true ;
2018-03-31 01:03:58 +01:00
}
2021-02-12 23:38:29 +00:00
if ( password ) {
2018-03-31 01:03:58 +01:00
// we had password and yet could not load/decrypt
2020-07-20 10:58:35 -04: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-20 18:14:02 -04:00
unlockAttempt = 0 ;
2020-07-20 10:58:35 -04:00
// Return true because there was no wallet data in keychain. Proceed.
return true ;
2018-03-31 01:03:58 +01:00
}
2020-10-24 13:20:59 -04:00
} ;
2018-03-31 14:43:08 +01:00
2024-01-28 11:11:08 -04:00
initCurrencyDaemon ( ) ;
2018-01-30 22:42:38 +00:00
2024-03-15 23:05:15 +03:00
export default BlueApp ;