2020-12-15 22:15:57 -05:00
import AsyncStorage from '@react-native-async-storage/async-storage' ;
2019-08-19 21:18:06 +01:00
import RNSecureKeyStore , { ACCESSIBLE } from 'react-native-secure-key-store' ;
2021-05-24 13:16:03 +01:00
import * as Keychain from 'react-native-keychain' ;
2018-07-22 15:49:59 +01:00
import {
HDLegacyBreadwalletWallet ,
HDSegwitP2SHWallet ,
HDLegacyP2PKHWallet ,
WatchOnlyWallet ,
LegacyWallet ,
SegwitP2SHWallet ,
SegwitBech32Wallet ,
2019-06-01 21:44:39 +01:00
HDSegwitBech32Wallet ,
2019-12-26 20:21:07 -06:00
LightningCustodianWallet ,
2020-04-22 16:13:18 +01:00
HDLegacyElectrumSeedP2PKHWallet ,
2020-05-04 18:52:01 +09:00
HDSegwitElectrumSeedP2WPKHWallet ,
2021-01-28 12:54:23 +00:00
HDAezeedWallet ,
2020-10-05 22:25:14 +01:00
MultisigHDWallet ,
2021-09-09 12:00:11 +01:00
LightningLdkWallet ,
2021-04-15 20:52:48 +03:00
SLIP39SegwitP2SHWallet ,
SLIP39LegacyP2PKHWallet ,
SLIP39SegwitBech32Wallet ,
2018-07-22 15:49:59 +01:00
} from './' ;
2021-05-24 13:16:03 +01:00
import { randomBytes } from './rng' ;
2021-10-04 02:02:33 -04:00
import alert from '../components/Alert' ;
2020-07-01 12:56:52 +01:00
const encryption = require ( '../blue_modules/encryption' ) ;
2020-08-28 12:41:56 +01:00
const Realm = require ( 'realm' ) ;
const createHash = require ( 'create-hash' ) ;
let usedBucketNum = false ;
2021-05-24 13:16:03 +01:00
let savingInProgress = 0 ; // its both a flag and a counter of attempts to write to disk
2018-03-20 22:41:07 +02:00
export class AppStorage {
2018-03-31 01:03:58 +01:00
static FLAG _ENCRYPTED = 'data_encrypted' ;
2018-12-11 22:52:46 +00:00
static LNDHUB = 'lndhub' ;
2019-05-19 20:49:42 +01:00
static ADVANCED _MODE _ENABLED = 'advancedmodeenabled' ;
2021-05-07 16:55:17 +01:00
static DO _NOT _TRACK = 'donottrack' ;
2021-01-18 22:40:11 -05:00
static HANDOFF _STORAGE _KEY = 'HandOff' ;
2020-03-20 15:25:23 +00:00
2021-05-18 21:38:18 +01:00
static keys2migrate = [ AppStorage . HANDOFF _STORAGE _KEY , AppStorage . DO _NOT _TRACK , AppStorage . ADVANCED _MODE _ENABLED ] ;
2018-03-20 22:41:07 +02:00
constructor ( ) {
/** {Array.<AbstractWallet>} */
this . wallets = [ ] ;
this . tx _metadata = { } ;
2018-03-31 01:03:58 +01:00
this . cachedPassword = false ;
2018-03-20 22:41:07 +02:00
}
2021-05-18 21:38:18 +01:00
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 ( _ ) { }
}
}
2019-08-19 21:18:06 +01:00
/ * *
* Wrapper for storage call . Secure store works only in RN environment . AsyncStorage is
* used for cli / tests
*
* @ param key
* @ param value
* @ returns { Promise < any > | Promise < any > | Promise < void > | * | Promise | void }
* /
2020-10-24 13:20:59 -04:00
setItem = ( key , value ) => {
2019-08-19 21:18:06 +01:00
if ( typeof navigator !== 'undefined' && navigator . product === 'ReactNative' ) {
2023-01-07 19:49:20 +02:00
return RNSecureKeyStore . set ( key , value , { accessible : ACCESSIBLE . WHEN _UNLOCKED _THIS _DEVICE _ONLY } ) ;
2019-08-19 21:18:06 +01:00
} else {
return AsyncStorage . setItem ( key , value ) ;
}
2020-11-10 18:28:26 +00:00
} ;
2019-08-19 21:18:06 +01:00
/ * *
* Wrapper for storage call . Secure store works only in RN environment . AsyncStorage is
* used for cli / tests
*
* @ param key
* @ returns { Promise < any > | * }
* /
2020-11-10 18:28:26 +00:00
getItem = key => {
2019-08-19 21:18:06 +01:00
if ( typeof navigator !== 'undefined' && navigator . product === 'ReactNative' ) {
return RNSecureKeyStore . get ( key ) ;
} else {
return AsyncStorage . getItem ( key ) ;
}
2020-11-10 18:28:26 +00:00
} ;
2019-08-19 21:18:06 +01:00
2021-05-24 13:16:03 +01:00
/ * *
* @ 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 ;
}
} ;
2020-10-24 13:20:59 -04:00
storageIsEncrypted = async ( ) => {
2018-03-30 19:31:10 +01:00
let data ;
try {
2021-05-24 13:16:03 +01:00
data = await this . getItemWithFallbackToRealm ( AppStorage . FLAG _ENCRYPTED ) ;
2018-03-30 19:31:10 +01:00
} catch ( error ) {
2021-05-24 13:16:03 +01:00
console . warn ( 'error reading `' + AppStorage . FLAG _ENCRYPTED + '` key:' , error . message ) ;
2018-03-30 19:31:10 +01:00
return false ;
}
2018-03-31 01:03:58 +01:00
return ! ! data ;
2020-10-24 13:20:59 -04:00
} ;
2018-03-31 01:03:58 +01:00
2020-10-24 13:20:59 -04:00
isPasswordInUse = async password => {
2019-08-23 03:04:23 -04:00
try {
let data = await this . getItem ( 'data' ) ;
data = this . decryptData ( data , password ) ;
2020-03-20 15:25:23 +00:00
return ! ! data ;
2019-08-23 03:04:23 -04:00
} catch ( _e ) {
return false ;
}
2020-10-24 13:20:59 -04:00
} ;
2019-08-23 03:04:23 -04:00
2018-03-31 01:03:58 +01:00
/ * *
* Iterates through all values of ` data ` trying to
* decrypt each one , and returns first one successfully decrypted
*
2020-03-20 15:25:23 +00:00
* @ param data { string } Serialized array
2018-03-31 01:03:58 +01:00
* @ param password
2020-03-20 15:25:23 +00:00
* @ returns { boolean | string } Either STRING of storage data ( which is stringified JSON ) or FALSE , which means failure
2018-03-31 01:03:58 +01:00
* /
decryptData ( data , password ) {
data = JSON . parse ( data ) ;
let decrypted ;
2020-08-28 12:41:56 +01:00
let num = 0 ;
2020-06-01 15:54:23 +03:00
for ( const value of data ) {
2021-02-12 23:38:29 +00:00
decrypted = encryption . decrypt ( value , password ) ;
2018-03-31 01:03:58 +01:00
if ( decrypted ) {
2020-08-28 12:41:56 +01:00
usedBucketNum = num ;
2018-03-31 01:03:58 +01:00
return decrypted ;
}
2020-08-28 12:41:56 +01:00
num ++ ;
2018-03-30 19:31:10 +01:00
}
2018-03-31 01:03:58 +01:00
return false ;
}
2020-10-24 13:20:59 -04:00
decryptStorage = async password => {
2019-11-10 09:18:56 -05:00
if ( password === this . cachedPassword ) {
this . cachedPassword = undefined ;
await this . saveToDisk ( ) ;
this . wallets = [ ] ;
this . tx _metadata = [ ] ;
return this . loadFromDisk ( ) ;
} else {
2020-11-22 04:20:31 -05:00
throw new Error ( 'Incorrect password. Please, try again.' ) ;
2019-08-23 03:04:23 -04:00
}
2020-10-24 13:20:59 -04:00
} ;
2019-08-23 03:04:23 -04:00
2020-10-24 13:20:59 -04:00
encryptStorage = async password => {
2018-03-31 01:03:58 +01:00
// assuming the storage is not yet encrypted
await this . saveToDisk ( ) ;
2019-08-19 21:18:06 +01:00
let data = await this . getItem ( 'data' ) ;
2018-03-31 01:03:58 +01:00
// TODO: refactor ^^^ (should not save & load to fetch data)
2020-06-01 15:54:23 +03:00
const encrypted = encryption . encrypt ( data , password ) ;
2018-03-31 01:03:58 +01:00
data = [ ] ;
data . push ( encrypted ) ; // putting in array as we might have many buckets with storages
data = JSON . stringify ( data ) ;
2018-04-01 00:16:42 +01:00
this . cachedPassword = password ;
2019-08-21 23:34:03 +01:00
await this . setItem ( 'data' , data ) ;
await this . setItem ( AppStorage . FLAG _ENCRYPTED , '1' ) ;
2020-10-24 13:20:59 -04:00
} ;
2018-03-30 19:31:10 +01:00
2018-04-01 00:16:42 +01:00
/ * *
* Cleans up all current application data ( wallets , tx metadata etc )
* Encrypts the bucket and saves it storage
*
* @ returns { Promise . < boolean > } Success or failure
* /
2020-10-24 13:20:59 -04:00
createFakeStorage = async fakePassword => {
2020-08-28 12:41:56 +01:00
usedBucketNum = false ; // resetting currently used bucket so we wont overwrite it
2018-04-01 00:16:42 +01:00
this . wallets = [ ] ;
this . tx _metadata = { } ;
2020-06-01 15:54:23 +03:00
const data = {
2018-04-01 00:16:42 +01:00
wallets : [ ] ,
tx _metadata : { } ,
} ;
2019-08-19 21:18:06 +01:00
let buckets = await this . getItem ( 'data' ) ;
2018-04-01 00:16:42 +01:00
buckets = JSON . parse ( buckets ) ;
buckets . push ( encryption . encrypt ( JSON . stringify ( data ) , fakePassword ) ) ;
this . cachedPassword = fakePassword ;
2019-05-02 16:33:03 -04:00
const bucketsString = JSON . stringify ( buckets ) ;
2019-08-21 23:34:03 +01:00
await this . setItem ( 'data' , bucketsString ) ;
2019-08-19 21:18:06 +01:00
return ( await this . getItem ( 'data' ) ) === bucketsString ;
2020-10-24 13:20:59 -04:00
} ;
2018-04-01 00:16:42 +01:00
2020-10-24 13:20:59 -04:00
hashIt = s => {
2020-08-28 12:41:56 +01:00
return createHash ( 'sha256' ) . update ( s ) . digest ( ) . toString ( 'hex' ) ;
2020-10-24 13:20:59 -04:00
} ;
2020-08-28 12:41:56 +01:00
/ * *
* 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 ) ;
2021-08-19 20:44:17 +01:00
const path = this . hashIt ( this . hashIt ( password ) ) + '-wallettransactions.realm' ;
2020-08-28 12:41:56 +01:00
const schema = [
{
2021-08-19 20:44:17 +01:00
name : 'WalletTransactions' ,
2020-08-28 12:41:56 +01:00
properties : {
walletid : { type : 'string' , indexed : true } ,
2021-08-19 20:44:17 +01:00
internal : 'bool?' , // true - internal, false - external
index : 'int?' ,
tx : 'string' , // stringified json
2020-08-28 12:41:56 +01:00
} ,
} ,
] ;
return Realm . open ( {
schema ,
path ,
encryptionKey ,
} ) ;
}
2021-05-24 13:16:03 +01:00
/ * *
* 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' ,
{
2022-10-31 12:25:26 +00:00
key ,
value ,
2021-05-24 13:16:03 +01:00
} ,
Realm . UpdateMode . Modified ,
) ;
} ) ;
}
2018-03-31 01:03:58 +01:00
/ * *
* Loads from storage all wallets and
* maps them to ` this.wallets `
*
* @ param password If present means storage must be decrypted before usage
* @ returns { Promise . < boolean > }
* /
async loadFromDisk ( password ) {
2021-05-24 13:16:03 +01:00
let data = await this . getItemWithFallbackToRealm ( 'data' ) ;
2021-02-12 23:38:29 +00:00
if ( password ) {
data = this . decryptData ( data , password ) ;
if ( data ) {
// password is good, cache it
this . cachedPassword = password ;
2018-03-31 01:03:58 +01:00
}
2021-02-12 23:38:29 +00:00
}
if ( data !== null ) {
2021-08-19 20:44:17 +01:00
let realm ;
try {
realm = await this . getRealm ( ) ;
} catch ( error ) {
alert ( error . message ) ;
}
2021-02-12 23:38:29 +00: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 ) ;
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 ( ) ) {
2019-12-26 20:21:07 -06:00
continue ;
2021-02-12 23:38:29 +00:00
}
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 ) ;
2021-07-12 11:55:28 +03: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 ;
}
2021-09-09 12:00:11 +01:00
break ;
case LightningLdkWallet . type :
unserializedWallet = LightningLdkWallet . fromJson ( key ) ;
2021-02-12 23:38:29 +00:00
break ;
2021-04-15 20:52:48 +03:00
case SLIP39SegwitP2SHWallet . type :
unserializedWallet = SLIP39SegwitP2SHWallet . fromJson ( key ) ;
break ;
case SLIP39LegacyP2PKHWallet . type :
unserializedWallet = SLIP39LegacyP2PKHWallet . fromJson ( key ) ;
break ;
case SLIP39SegwitBech32Wallet . type :
unserializedWallet = SLIP39SegwitBech32Wallet . fromJson ( key ) ;
break ;
2021-02-12 23:38:29 +00:00
case LightningCustodianWallet . type : {
/** @type {LightningCustodianWallet} */
unserializedWallet = LightningCustodianWallet . fromJson ( key ) ;
let lndhub = false ;
try {
lndhub = await AsyncStorage . getItem ( AppStorage . LNDHUB ) ;
} catch ( Error ) {
console . warn ( Error ) ;
}
2019-03-08 21:54:47 +00:00
2021-02-12 23:38:29 +00: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 {
2021-07-08 13:39:03 -04:00
console . log ( 'wallet does not have a baseURI. Continuing init...' ) ;
2020-06-01 15:54:23 +03:00
}
2021-02-12 23:38:29 +00:00
unserializedWallet . init ( ) ;
break ;
2018-03-20 22:41:07 +02:00
}
2021-02-12 23:38:29 +00:00
case LegacyWallet . type :
default :
unserializedWallet = LegacyWallet . fromJson ( key ) ;
break ;
}
2020-08-28 12:41:56 +01:00
2021-08-23 12:30:48 +01:00
try {
if ( realm ) this . inflateWalletFromRealm ( realm , unserializedWallet ) ;
} catch ( error ) {
alert ( error . message ) ;
}
2020-08-28 12:41:56 +01:00
2021-02-12 23:38:29 +00:00
// done
2021-09-19 14:01:22 +03:00
const ID = unserializedWallet . getID ( ) ;
if ( ! this . wallets . some ( wallet => wallet . getID ( ) === ID ) ) {
2021-02-12 23:38:29 +00:00
this . wallets . push ( unserializedWallet ) ;
this . tx _metadata = data . tx _metadata ;
2018-03-20 22:41:07 +02:00
}
}
2021-08-19 20:44:17 +01:00
if ( realm ) realm . close ( ) ;
2021-02-12 23:38:29 +00:00
return true ;
} else {
return false ; // failed loading data or loading/decryptin data
2018-03-20 22:41:07 +02:00
}
}
/ * *
2018-07-02 10:48:40 +01:00
* Lookup wallet in list by it ' s secret and
* remove it from ` this.wallets `
2018-03-20 22:41:07 +02:00
*
* @ param wallet { AbstractWallet }
* /
2020-10-24 13:20:59 -04:00
deleteWallet = wallet => {
2021-09-19 14:01:22 +03:00
const ID = wallet . getID ( ) ;
2020-06-01 15:54:23 +03:00
const tempWallets = [ ] ;
2019-11-11 01:26:39 -05:00
2021-09-09 12:00:11 +01:00
if ( wallet . type === LightningLdkWallet . type ) {
/** @type {LightningLdkWallet} */
const ldkwallet = wallet ;
2022-04-19 15:37:07 -04:00
ldkwallet . stop ( ) . then ( ldkwallet . purgeLocalStorage ) . catch ( alert ) ;
2021-09-09 12:00:11 +01:00
}
2020-06-01 15:54:23 +03:00
for ( const value of this . wallets ) {
2021-09-23 16:05:10 +03:00
if ( value . getID ( ) === ID ) {
2018-03-20 22:41:07 +02:00
// the one we should delete
// nop
} else {
// the one we must keep
tempWallets . push ( value ) ;
}
}
this . wallets = tempWallets ;
2020-10-24 13:20:59 -04:00
} ;
2018-03-20 22:41:07 +02:00
2020-08-28 12:41:56 +01:00
inflateWalletFromRealm ( realm , walletToInflate ) {
2021-08-19 20:44:17 +01:00
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 ) ) ;
}
2021-08-23 12:30:48 +01:00
} else if ( tx . internal === true ) {
2021-08-19 20:44:17 +01:00
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 ) ) ;
2020-08-28 12:41:56 +01:00
}
2021-08-23 12:30:48 +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 || [ ] ;
walletToInflate . _txs _by _external _index . push ( JSON . parse ( tx . tx ) ) ;
2020-08-28 12:41:56 +01:00
}
}
}
offloadWalletToRealm ( realm , wallet ) {
const id = wallet . getID ( ) ;
const walletToSave = wallet . _hdWalletInstance ? ? wallet ;
2021-08-23 12:30:48 +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 ) ;
for ( const tx of walletToSave . _txs _by _external _index ) {
realm . create (
'WalletTransactions' ,
{
walletid : id ,
tx : JSON . stringify ( tx ) ,
} ,
Realm . UpdateMode . Modified ,
) ;
}
} ) ;
return ;
}
/// ########################################################################################################
2020-11-10 18:28:26 +00:00
if ( walletToSave . _txs _by _external _index ) {
2020-08-28 12:41:56 +01:00
realm . write ( ( ) => {
2021-08-19 20:44:17 +01:00
// 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 ,
index : parseInt ( index ) ,
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 ,
index : parseInt ( index ) ,
tx : JSON . stringify ( tx ) ,
} ,
Realm . UpdateMode . Modified ,
) ;
}
}
2020-08-28 12:41:56 +01:00
} ) ;
}
}
2018-03-31 01:03:58 +01:00
/ * *
* Serializes and saves to storage object data .
* If cached password is saved - finds the correct bucket
* to save to , encrypts and then saves .
*
2019-08-19 21:18:06 +01:00
* @ returns { Promise } Result of storage save
2018-03-31 01:03:58 +01:00
* /
async saveToDisk ( ) {
2021-05-24 13:16:03 +01:00
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 ( ) ;
2018-03-20 22:41:07 +02:00
}
2021-05-24 13:16:03 +01:00
savingInProgress = 1 ;
2018-03-20 22:41:07 +02:00
2021-05-24 13:16:03 +01:00
try {
const walletsToSave = [ ] ;
2021-08-19 20:44:17 +01:00
let realm ;
try {
realm = await this . getRealm ( ) ;
} catch ( error ) {
alert ( error . message ) ;
}
2021-05-24 13:16:03 +01:00
for ( const key of this . wallets ) {
2021-09-23 16:05:10 +03:00
if ( typeof key === 'boolean' ) continue ;
2021-05-24 13:16:03 +01:00
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 ) ;
2021-08-19 20:44:17 +01:00
if ( realm ) this . offloadWalletToRealm ( realm , key ) ;
2021-05-24 13:16:03 +01:00
// stripping down:
if ( key . _txs _by _external _index ) {
keyCloned . _txs _by _external _index = { } ;
keyCloned . _txs _by _internal _index = { } ;
2020-08-28 12:41:56 +01:00
}
2021-05-24 13:16:03 +01:00
if ( key . _hdWalletInstance ) {
keyCloned . _hdWalletInstance . _txs _by _external _index = { } ;
keyCloned . _hdWalletInstance . _txs _by _internal _index = { } ;
}
2023-03-26 19:43:25 +01:00
if ( keyCloned . _bip47 _instance ) {
delete keyCloned . _bip47 _instance ; // since it wont be restored into a proper class instance
}
2021-05-24 13:16:03 +01:00
walletsToSave . push ( JSON . stringify ( { ... keyCloned , type : keyCloned . type } ) ) ;
}
2021-08-19 20:44:17 +01:00
if ( realm ) realm . close ( ) ;
2021-05-24 13:16:03 +01:00
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 ) ;
}
2020-08-28 12:41:56 +01:00
2021-05-24 13:16:03 +01:00
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 ) ) ;
}
2018-03-31 01:03:58 +01:00
}
2021-05-24 13:16:03 +01:00
data = newData ;
2018-03-31 01:03:58 +01:00
}
2021-05-24 13:16:03 +01:00
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 ( ) ;
2020-07-02 14:43:35 +01:00
} catch ( error ) {
2021-05-24 13:16:03 +01:00
console . error ( 'save to disk exception:' , error . message ) ;
alert ( 'save to disk exception: ' + error . message ) ;
2021-08-15 11:16:50 +01:00
if ( error . message . includes ( 'Realm file decryption failed' ) ) {
console . warn ( 'purging realm key-value database file' ) ;
this . purgeRealmKeyValueFile ( ) ;
}
2021-05-24 13:16:03 +01:00
} finally {
savingInProgress = 0 ;
2020-07-02 14:43:35 +01:00
}
2018-03-20 22:41:07 +02:00
}
2018-06-17 11:46:19 +01:00
/ * *
* For each wallet , fetches balance from remote endpoint .
* Use getter for a specific wallet to get actual balance .
* Returns void .
2018-06-28 02:43:28 +01:00
* If index is present then fetch only from this specific wallet
2018-06-17 11:46:19 +01:00
*
* @ return { Promise . < void > }
* /
2020-10-24 13:20:59 -04:00
fetchWalletBalances = async index => {
2020-05-19 18:16:30 +01:00
console . log ( 'fetchWalletBalances for wallet#' , typeof index === 'undefined' ? '(all)' : index ) ;
2018-06-28 02:43:28 +01:00
if ( index || index === 0 ) {
let c = 0 ;
2021-09-23 16:05:10 +03:00
for ( const wallet of this . wallets ) {
2018-06-28 02:43:28 +01:00
if ( c ++ === index ) {
await wallet . fetchBalance ( ) ;
}
}
} else {
2021-09-23 16:05:10 +03:00
for ( const wallet of this . wallets ) {
2022-12-12 22:09:04 -05:00
console . log ( 'fetching balance for' , wallet . getLabel ( ) ) ;
2018-06-28 02:43:28 +01:00
await wallet . fetchBalance ( ) ;
}
2018-03-20 22:41:07 +02:00
}
2020-10-24 13:20:59 -04:00
} ;
2018-03-20 22:41:07 +02:00
2018-06-17 11:46:19 +01:00
/ * *
* Fetches from remote endpoint all transactions for each wallet .
* Returns void .
* To access transactions - get them from each respective wallet .
2018-06-28 02:43:28 +01:00
* If index is present then fetch only from this specific wallet .
2018-06-17 11:46:19 +01:00
*
2018-06-24 23:22:46 +01:00
* @ param index { Integer } Index of the wallet in this . wallets array ,
* blank to fetch from all wallets
2018-06-17 11:46:19 +01:00
* @ return { Promise . < void > }
* /
2020-10-24 13:20:59 -04:00
fetchWalletTransactions = async index => {
2020-05-19 18:16:30 +01:00
console . log ( 'fetchWalletTransactions for wallet#' , typeof index === 'undefined' ? '(all)' : index ) ;
2018-06-24 23:22:46 +01:00
if ( index || index === 0 ) {
let c = 0 ;
2021-09-23 16:05:10 +03:00
for ( const wallet of this . wallets ) {
2018-06-24 23:22:46 +01:00
if ( c ++ === index ) {
await wallet . fetchTransactions ( ) ;
2018-12-25 19:07:43 +00:00
if ( wallet . fetchPendingTransactions ) {
await wallet . fetchPendingTransactions ( ) ;
}
if ( wallet . fetchUserInvoices ) {
await wallet . fetchUserInvoices ( ) ;
}
2018-06-24 23:22:46 +01:00
}
}
} else {
2020-06-01 15:54:23 +03:00
for ( const wallet of this . wallets ) {
2018-06-24 23:22:46 +01:00
await wallet . fetchTransactions ( ) ;
2018-12-25 19:07:43 +00:00
if ( wallet . fetchPendingTransactions ) {
await wallet . fetchPendingTransactions ( ) ;
}
if ( wallet . fetchUserInvoices ) {
await wallet . fetchUserInvoices ( ) ;
}
2018-06-24 23:22:46 +01:00
}
2018-03-20 22:41:07 +02:00
}
2020-10-24 13:20:59 -04:00
} ;
2018-03-20 22:41:07 +02:00
2022-12-11 21:34:50 -05:00
fetchSenderPaymentCodes = async index => {
console . log ( 'fetchSenderPaymentCodes for wallet#' , typeof index === 'undefined' ? '(all)' : index ) ;
if ( index || index === 0 ) {
2022-12-12 22:09:04 -05:00
try {
2023-03-26 19:43:25 +01:00
if ( ! ( this . wallets [ index ] . allowBIP47 ( ) && this . wallets [ index ] . isBIP47Enabled ( ) ) ) return ;
2022-12-12 22:09:04 -05:00
await this . wallets [ index ] . fetchBIP47SenderPaymentCodes ( ) ;
} catch ( error ) {
console . error ( 'Failed to fetch sender payment codes for wallet' , index , error ) ;
}
2022-12-11 21:34:50 -05:00
} else {
for ( const wallet of this . wallets ) {
2022-12-12 22:09:04 -05:00
try {
2023-03-26 19:43:25 +01:00
if ( ! ( wallet . allowBIP47 ( ) && wallet . isBIP47Enabled ( ) ) ) continue ;
2022-12-12 22:09:04 -05:00
await wallet . fetchBIP47SenderPaymentCodes ( ) ;
} catch ( error ) {
console . error ( 'Failed to fetch sender payment codes for wallet' , wallet . label , error ) ;
}
2022-12-11 21:34:50 -05:00
}
}
} ;
2018-03-20 22:41:07 +02:00
/ * *
*
* @ returns { Array . < AbstractWallet > }
* /
2020-10-24 13:20:59 -04:00
getWallets = ( ) => {
2018-03-20 22:41:07 +02:00
return this . wallets ;
2020-10-24 13:20:59 -04:00
} ;
2018-03-20 22:41:07 +02:00
2018-06-17 11:46:19 +01:00
/ * *
2018-06-24 23:22:46 +01:00
* Getter for all transactions in all wallets .
* But if index is provided - only for wallet with corresponding index
2018-06-17 11:46:19 +01:00
*
2019-02-18 23:37:53 +00:00
* @ param index { Integer | null } Wallet index in this . wallets . Empty ( or null ) for all wallets .
* @ param limit { Integer } How many txs return , starting from the earliest . Default : all of them .
2020-06-18 10:59:44 -04:00
* @ param includeWalletsWithHideTransactionsEnabled { Boolean } Wallets ' _hideTransactionsInWalletsList property determines wether the user wants this wallet' s txs hidden from the main list view .
2018-06-17 11:46:19 +01:00
* @ return { Array }
* /
2020-10-24 13:20:59 -04:00
getTransactions = ( index , limit = Infinity , includeWalletsWithHideTransactionsEnabled = false ) => {
2018-06-24 23:22:46 +01:00
if ( index || index === 0 ) {
let txs = [ ] ;
let c = 0 ;
2020-06-01 15:54:23 +03:00
for ( const wallet of this . wallets ) {
2018-06-24 23:22:46 +01:00
if ( c ++ === index ) {
2018-07-22 15:49:59 +01:00
txs = txs . concat ( wallet . getTransactions ( ) ) ;
2018-06-24 23:22:46 +01:00
}
}
return txs ;
}
2018-03-20 22:41:07 +02:00
let txs = [ ] ;
2020-06-18 10:59:44 -04:00
for ( const wallet of this . wallets . filter ( w => includeWalletsWithHideTransactionsEnabled || ! w . getHideTransactionsInWalletsList ( ) ) ) {
2020-06-01 15:54:23 +03:00
const walletTransactions = wallet . getTransactions ( ) ;
2021-12-17 05:41:18 -05:00
const walletID = wallet . getID ( ) ;
2020-06-01 15:54:23 +03:00
for ( const t of walletTransactions ) {
2019-01-06 23:54:22 -05:00
t . walletPreferredBalanceUnit = wallet . getPreferredBalanceUnit ( ) ;
2021-12-17 05:41:18 -05:00
t . walletID = walletID ;
2019-01-06 23:54:22 -05:00
}
txs = txs . concat ( walletTransactions ) ;
2018-03-20 22:41:07 +02:00
}
2018-10-09 00:25:36 -04:00
2020-06-01 15:54:23 +03:00
for ( const t of txs ) {
2018-10-09 00:25:36 -04:00
t . sort _ts = + new Date ( t . received ) ;
}
2019-02-18 23:37:53 +00:00
return txs
2020-06-01 15:54:23 +03:00
. sort ( function ( a , b ) {
2019-02-18 23:37:53 +00:00
return b . sort _ts - a . sort _ts ;
} )
. slice ( 0 , limit ) ;
2020-10-24 13:20:59 -04:00
} ;
2018-03-20 22:41:07 +02:00
2018-06-17 11:46:19 +01:00
/ * *
* Getter for a sum of all balances of all wallets
*
* @ return { number }
* /
2020-10-24 13:20:59 -04:00
getBalance = ( ) => {
2018-03-20 22:41:07 +02:00
let finalBalance = 0 ;
2020-06-01 15:54:23 +03:00
for ( const wal of this . wallets ) {
2019-11-28 23:16:04 +00:00
finalBalance += wal . getBalance ( ) ;
2018-03-20 22:41:07 +02:00
}
return finalBalance ;
2020-10-24 13:20:59 -04:00
} ;
2019-07-19 01:22:03 +01:00
2023-03-04 11:30:51 -04:00
isAdvancedModeEnabled = async ( ) => {
2020-03-20 15:25:23 +00:00
try {
2021-05-18 21:38:18 +01:00
return ! ! ( await AsyncStorage . getItem ( AppStorage . ADVANCED _MODE _ENABLED ) ) ;
2020-03-20 15:25:23 +00:00
} catch ( _ ) { }
return false ;
2020-10-24 13:20:59 -04:00
} ;
2020-03-20 15:25:23 +00:00
2023-03-04 11:30:51 -04:00
setIsAdvancedModeEnabled = async value => {
2021-05-18 21:38:18 +01:00
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' : '' ) ;
2020-10-24 13:20:59 -04:00
} ;
2020-03-20 15:25:23 +00:00
2021-05-07 16:55:17 +01:00
isDoNotTrackEnabled = async ( ) => {
try {
2021-05-18 21:38:18 +01:00
return ! ! ( await AsyncStorage . getItem ( AppStorage . DO _NOT _TRACK ) ) ;
2021-05-07 16:55:17 +01:00
} catch ( _ ) { }
return false ;
} ;
setDoNotTrack = async value => {
2021-05-18 21:38:18 +01:00
await AsyncStorage . setItem ( AppStorage . DO _NOT _TRACK , value ? '1' : '' ) ;
2021-05-07 16:55:17 +01:00
} ;
2019-07-19 01:22:03 +01:00
/ * *
* Simple async sleeper function
*
* @ param ms { number } Milliseconds to sleep
* @ returns { Promise < Promise < * > | Promise < * >> }
* /
2020-10-24 13:20:59 -04:00
sleep = ms => {
2019-07-19 01:22:03 +01:00
return new Promise ( resolve => setTimeout ( resolve , ms ) ) ;
2020-10-24 13:20:59 -04:00
} ;
2021-08-15 11:16:50 +01:00
purgeRealmKeyValueFile ( ) {
const path = 'keyvalue.realm' ;
return Realm . deleteFile ( {
path ,
} ) ;
}
2019-11-10 09:18:56 -05:00
}