feat: BlueApp TS

This commit is contained in:
Ivan Vershigora 2024-03-15 23:05:15 +03:00 committed by Overtorment
parent 52ff33ad2b
commit e1c99f19b4
43 changed files with 709 additions and 531 deletions

View File

@ -1,41 +1,61 @@
import Biometric from './class/biometrics';
import { Platform } from 'react-native';
import loc from './loc';
import AsyncStorage from '@react-native-async-storage/async-storage';
import RNSecureKeyStore, { ACCESSIBLE } from 'react-native-secure-key-store';
import createHash from 'create-hash';
import { Platform } from 'react-native';
import DefaultPreference from 'react-native-default-preference';
import * as Keychain from 'react-native-keychain';
import RNSecureKeyStore, { ACCESSIBLE } from 'react-native-secure-key-store';
import Realm from 'realm';
import BlueElectrum from './blue_modules/BlueElectrum';
import { initCurrencyDaemon } from './blue_modules/currency';
import {
HDLegacyBreadwalletWallet,
HDSegwitP2SHWallet,
HDLegacyP2PKHWallet,
WatchOnlyWallet,
LegacyWallet,
SegwitP2SHWallet,
SegwitBech32Wallet,
HDSegwitBech32Wallet,
LightningCustodianWallet,
HDLegacyElectrumSeedP2PKHWallet,
HDSegwitElectrumSeedP2WPKHWallet,
HDAezeedWallet,
MultisigHDWallet,
HDLegacyBreadwalletWallet,
HDLegacyElectrumSeedP2PKHWallet,
HDLegacyP2PKHWallet,
HDSegwitBech32Wallet,
HDSegwitElectrumSeedP2WPKHWallet,
HDSegwitP2SHWallet,
LegacyWallet,
LightningCustodianWallet,
LightningLdkWallet,
SLIP39SegwitP2SHWallet,
MultisigHDWallet,
SLIP39LegacyP2PKHWallet,
SLIP39SegwitBech32Wallet,
SLIP39SegwitP2SHWallet,
SegwitBech32Wallet,
SegwitP2SHWallet,
WatchOnlyWallet,
} from './class/';
import Biometric from './class/biometrics';
import { randomBytes } from './class/rng';
import { TWallet, Transaction } from './class/wallets/types';
import presentAlert from './components/Alert';
import { initCurrencyDaemon } from './blue_modules/currency';
import DefaultPreference from 'react-native-default-preference';
import loc from './loc';
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
const prompt = require('./helpers/prompt');
const encryption = require('./blue_modules/encryption');
class AppStorage {
let usedBucketNum: boolean | number = false;
let savingInProgress = 0; // its both a flag and a counter of attempts to write to disk
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';
export class AppStorage {
static FLAG_ENCRYPTED = 'data_encrypted';
static LNDHUB = 'lndhub';
static ADVANCED_MODE_ENABLED = 'advancedmodeenabled';
@ -44,16 +64,23 @@ class AppStorage {
static keys2migrate = [AppStorage.HANDOFF_STORAGE_KEY, AppStorage.DO_NOT_TRACK, AppStorage.ADVANCED_MODE_ENABLED];
public cachedPassword?: false | string;
public tx_metadata: TTXMetadata;
public wallets: TWallet[];
constructor() {
/** {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) {
// do not migrate keys if we are not in RN env
if (!isReactNative) {
return;
}
for (const key of AppStorage.keys2migrate) {
try {
const value = await RNSecureKeyStore.get(key);
if (value) {
@ -67,13 +94,9 @@ class AppStorage {
/**
* 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') {
setItem = (key: string, value: any): Promise<any> => {
if (isReactNative) {
return RNSecureKeyStore.set(key, value, { accessible: ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY });
} else {
return AsyncStorage.setItem(key, value);
@ -83,28 +106,20 @@ class AppStorage {
/**
* 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') {
getItem = (key: string): Promise<any> => {
if (isReactNative) {
return RNSecureKeyStore.get(key);
} else {
return AsyncStorage.getItem(key);
}
};
/**
* @throws Error
* @param key {string}
* @returns {Promise<*>|null}
*/
getItemWithFallbackToRealm = async key => {
getItemWithFallbackToRealm = async (key: string): Promise<any | null> => {
let value;
try {
return await this.getItem(key);
} catch (error) {
} catch (error: any) {
console.warn('error reading', key, error.message);
console.warn('fallback to realm');
const realmKeyValue = await this.openRealmKeyValue();
@ -112,6 +127,7 @@ class AppStorage {
value = obj?.value;
realmKeyValue.close();
if (value) {
// @ts-ignore value.length
console.warn('successfully recovered', value.length, 'bytes from realm for key', key);
return value;
}
@ -119,23 +135,23 @@ class AppStorage {
}
};
storageIsEncrypted = async () => {
storageIsEncrypted = async (): Promise<boolean> => {
let data;
try {
data = await this.getItemWithFallbackToRealm(AppStorage.FLAG_ENCRYPTED);
} catch (error) {
} catch (error: any) {
console.warn('error reading `' + AppStorage.FLAG_ENCRYPTED + '` key:', error.message);
return false;
}
return !!data;
return Boolean(data);
};
isPasswordInUse = async password => {
isPasswordInUse = async (password: string) => {
try {
let data = await this.getItem('data');
data = this.decryptData(data, password);
return !!data;
return Boolean(data);
} catch (_e) {
return false;
}
@ -144,12 +160,8 @@ class AppStorage {
/**
* 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) {
decryptData(data: string, password: string): boolean | string {
data = JSON.parse(data);
let decrypted;
let num = 0;
@ -166,19 +178,19 @@ class AppStorage {
return false;
}
decryptStorage = async password => {
decryptStorage = async (password: string): Promise<boolean> => {
if (password === this.cachedPassword) {
this.cachedPassword = undefined;
this.cachedPassword = undefined; // maybe reset to false ?
await this.saveToDisk();
this.wallets = [];
this.tx_metadata = [];
this.tx_metadata = {};
return this.loadFromDisk();
} else {
throw new Error('Incorrect password. Please, try again.');
}
};
encryptStorage = async password => {
encryptStorage = async (password: string): Promise<void> => {
// assuming the storage is not yet encrypted
await this.saveToDisk();
let data = await this.getItem('data');
@ -196,10 +208,8 @@ class AppStorage {
/**
* 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 => {
createFakeStorage = async (fakePassword: string): Promise<boolean> => {
usedBucketNum = false; // resetting currently used bucket so we wont overwrite it
this.wallets = [];
this.tx_metadata = {};
@ -218,15 +228,13 @@ class AppStorage {
return (await this.getItem('data')) === bucketsString;
};
hashIt = s => {
hashIt = (s: string): string => {
return createHash('sha256').update(s).digest().toString('hex');
};
/**
* Returns instace of the Realm database, which is encrypted either by cached user's password OR default password.
* Database file is deterministically derived from encryption key.
*
* @returns {Promise<Realm>}
*/
async getRealm() {
const password = this.hashIt(this.cachedPassword || 'fyegjitkyf[eqjnc.lf');
@ -245,7 +253,9 @@ class AppStorage {
},
},
];
// @ts-ignore schema doesn't match Realm's schema type
return Realm.open({
// @ts-ignore schema doesn't match Realm's schema type
schema,
path,
encryptionKey,
@ -258,7 +268,7 @@ class AppStorage {
*
* @returns {Promise<Realm>}
*/
async openRealmKeyValue() {
async openRealmKeyValue(): Promise<Realm> {
const service = 'realm_encryption_key';
let password;
const credentials = await Keychain.getGenericPassword({ service });
@ -284,14 +294,16 @@ class AppStorage {
},
},
];
// @ts-ignore schema doesn't match Realm's schema type
return Realm.open({
// @ts-ignore schema doesn't match Realm's schema type
schema,
path,
encryptionKey,
});
}
saveToRealmKeyValue(realmkeyValue, key, value) {
saveToRealmKeyValue(realmkeyValue: Realm, key: string, value: any) {
realmkeyValue.write(() => {
realmkeyValue.create(
'KeyValue',
@ -311,7 +323,7 @@ class AppStorage {
* @param password If present means storage must be decrypted before usage
* @returns {Promise.<boolean>}
*/
async loadFromDisk(password) {
async loadFromDisk(password?: string): Promise<boolean> {
let data = await this.getItemWithFallbackToRealm('data');
if (password) {
data = this.decryptData(data, password);
@ -324,7 +336,7 @@ class AppStorage {
let realm;
try {
realm = await this.getRealm();
} catch (error) {
} catch (error: any) {
presentAlert({ message: error.message });
}
data = JSON.parse(data);
@ -333,44 +345,44 @@ class AppStorage {
for (const key of wallets) {
// deciding which type is wallet and instatiating correct object
const tempObj = JSON.parse(key);
let unserializedWallet;
let unserializedWallet: TWallet;
switch (tempObj.type) {
case SegwitBech32Wallet.type:
unserializedWallet = SegwitBech32Wallet.fromJson(key);
unserializedWallet = SegwitBech32Wallet.fromJson(key) as unknown as SegwitBech32Wallet;
break;
case SegwitP2SHWallet.type:
unserializedWallet = SegwitP2SHWallet.fromJson(key);
unserializedWallet = SegwitP2SHWallet.fromJson(key) as unknown as SegwitP2SHWallet;
break;
case WatchOnlyWallet.type:
unserializedWallet = WatchOnlyWallet.fromJson(key);
unserializedWallet = WatchOnlyWallet.fromJson(key) as unknown as WatchOnlyWallet;
unserializedWallet.init();
if (unserializedWallet.isHd() && !unserializedWallet.isXpubValid()) {
continue;
}
break;
case HDLegacyP2PKHWallet.type:
unserializedWallet = HDLegacyP2PKHWallet.fromJson(key);
unserializedWallet = HDLegacyP2PKHWallet.fromJson(key) as unknown as HDLegacyP2PKHWallet;
break;
case HDSegwitP2SHWallet.type:
unserializedWallet = HDSegwitP2SHWallet.fromJson(key);
unserializedWallet = HDSegwitP2SHWallet.fromJson(key) as unknown as HDSegwitP2SHWallet;
break;
case HDSegwitBech32Wallet.type:
unserializedWallet = HDSegwitBech32Wallet.fromJson(key);
unserializedWallet = HDSegwitBech32Wallet.fromJson(key) as unknown as HDSegwitBech32Wallet;
break;
case HDLegacyBreadwalletWallet.type:
unserializedWallet = HDLegacyBreadwalletWallet.fromJson(key);
unserializedWallet = HDLegacyBreadwalletWallet.fromJson(key) as unknown as HDLegacyBreadwalletWallet;
break;
case HDLegacyElectrumSeedP2PKHWallet.type:
unserializedWallet = HDLegacyElectrumSeedP2PKHWallet.fromJson(key);
unserializedWallet = HDLegacyElectrumSeedP2PKHWallet.fromJson(key) as unknown as HDLegacyElectrumSeedP2PKHWallet;
break;
case HDSegwitElectrumSeedP2WPKHWallet.type:
unserializedWallet = HDSegwitElectrumSeedP2WPKHWallet.fromJson(key);
unserializedWallet = HDSegwitElectrumSeedP2WPKHWallet.fromJson(key) as unknown as HDSegwitElectrumSeedP2WPKHWallet;
break;
case MultisigHDWallet.type:
unserializedWallet = MultisigHDWallet.fromJson(key);
unserializedWallet = MultisigHDWallet.fromJson(key) as unknown as MultisigHDWallet;
break;
case HDAezeedWallet.type:
unserializedWallet = HDAezeedWallet.fromJson(key);
unserializedWallet = HDAezeedWallet.fromJson(key) as unknown as HDAezeedWallet;
// migrate password to this.passphrase field
// remove this code somewhere in year 2022
if (unserializedWallet.secret.includes(':')) {
@ -381,21 +393,20 @@ class AppStorage {
break;
case LightningLdkWallet.type:
unserializedWallet = LightningLdkWallet.fromJson(key);
unserializedWallet = LightningLdkWallet.fromJson(key) as unknown as LightningLdkWallet;
break;
case SLIP39SegwitP2SHWallet.type:
unserializedWallet = SLIP39SegwitP2SHWallet.fromJson(key);
unserializedWallet = SLIP39SegwitP2SHWallet.fromJson(key) as unknown as SLIP39SegwitP2SHWallet;
break;
case SLIP39LegacyP2PKHWallet.type:
unserializedWallet = SLIP39LegacyP2PKHWallet.fromJson(key);
unserializedWallet = SLIP39LegacyP2PKHWallet.fromJson(key) as unknown as SLIP39LegacyP2PKHWallet;
break;
case SLIP39SegwitBech32Wallet.type:
unserializedWallet = SLIP39SegwitBech32Wallet.fromJson(key);
unserializedWallet = SLIP39SegwitBech32Wallet.fromJson(key) as unknown as SLIP39SegwitBech32Wallet;
break;
case LightningCustodianWallet.type: {
/** @type {LightningCustodianWallet} */
unserializedWallet = LightningCustodianWallet.fromJson(key);
let lndhub = false;
unserializedWallet = LightningCustodianWallet.fromJson(key) as unknown as LightningCustodianWallet;
let lndhub: false | any = false;
try {
lndhub = await AsyncStorage.getItem(AppStorage.LNDHUB);
} catch (error) {
@ -416,13 +427,13 @@ class AppStorage {
}
case LegacyWallet.type:
default:
unserializedWallet = LegacyWallet.fromJson(key);
unserializedWallet = LegacyWallet.fromJson(key) as unknown as LegacyWallet;
break;
}
try {
if (realm) this.inflateWalletFromRealm(realm, unserializedWallet);
} catch (error) {
} catch (error: any) {
presentAlert({ message: error.message });
}
@ -446,12 +457,11 @@ class AppStorage {
*
* @param wallet {AbstractWallet}
*/
deleteWallet = wallet => {
deleteWallet = (wallet: TWallet): void => {
const ID = wallet.getID();
const tempWallets = [];
if (wallet.type === LightningLdkWallet.type) {
/** @type {LightningLdkWallet} */
const ldkwallet = wallet;
ldkwallet.stop().then(ldkwallet.purgeLocalStorage).catch(alert);
}
@ -468,39 +478,44 @@ class AppStorage {
this.wallets = tempWallets;
};
inflateWalletFromRealm(realm, walletToInflate) {
inflateWalletFromRealm(realm: Realm, walletToInflate: TWallet) {
const transactions = realm.objects('WalletTransactions');
const transactionsForWallet = transactions.filtered(`walletid = "${walletToInflate.getID()}"`);
const transactionsForWallet = transactions.filtered(`walletid = "${walletToInflate.getID()}"`) as unknown as TRealmTransaction[];
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));
if ('_hdWalletInstance' in walletToInflate && walletToInflate._hdWalletInstance) {
const hd = walletToInflate._hdWalletInstance;
hd._txs_by_external_index[tx.index] = hd._txs_by_external_index[tx.index] || [];
const transaction = JSON.parse(tx.tx);
hd._txs_by_external_index[tx.index].push(transaction);
} else {
walletToInflate._txs_by_external_index[tx.index] = walletToInflate._txs_by_external_index[tx.index] || [];
walletToInflate._txs_by_external_index[tx.index].push(JSON.parse(tx.tx));
const transaction = JSON.parse(tx.tx);
(walletToInflate._txs_by_external_index[tx.index] as Transaction[]).push(transaction);
}
} 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));
if ('_hdWalletInstance' in walletToInflate && walletToInflate._hdWalletInstance) {
const hd = walletToInflate._hdWalletInstance;
hd._txs_by_internal_index[tx.index] = hd._txs_by_internal_index[tx.index] || [];
const transaction = JSON.parse(tx.tx);
hd._txs_by_internal_index[tx.index].push(transaction);
} else {
walletToInflate._txs_by_internal_index[tx.index] = walletToInflate._txs_by_internal_index[tx.index] || [];
walletToInflate._txs_by_internal_index[tx.index].push(JSON.parse(tx.tx));
const transaction = JSON.parse(tx.tx);
(walletToInflate._txs_by_internal_index[tx.index] as Transaction[]).push(transaction);
}
} else {
if (!Array.isArray(walletToInflate._txs_by_external_index)) walletToInflate._txs_by_external_index = [];
walletToInflate._txs_by_external_index = walletToInflate._txs_by_external_index || [];
walletToInflate._txs_by_external_index.push(JSON.parse(tx.tx));
const transaction = JSON.parse(tx.tx);
(walletToInflate._txs_by_external_index as Transaction[]).push(transaction);
}
}
}
offloadWalletToRealm(realm, wallet) {
offloadWalletToRealm(realm: Realm, wallet: TWallet): void {
const id = wallet.getID();
const walletToSave = wallet._hdWalletInstance ?? wallet;
const walletToSave = ('_hdWalletInstance' in wallet && wallet._hdWalletInstance) || wallet;
if (Array.isArray(walletToSave._txs_by_external_index)) {
// if this var is an array that means its a single-address wallet class, and this var is a flat array
@ -510,6 +525,7 @@ class AppStorage {
const walletTransactionsToDelete = realm.objects('WalletTransactions').filtered(`walletid = '${id}'`);
realm.delete(walletTransactionsToDelete);
// @ts-ignore walletToSave._txs_by_external_index is array
for (const tx of walletToSave._txs_by_external_index) {
realm.create(
'WalletTransactions',
@ -535,6 +551,7 @@ class AppStorage {
// insert new ones:
for (const index of Object.keys(walletToSave._txs_by_external_index)) {
// @ts-ignore index is number
const txs = walletToSave._txs_by_external_index[index];
for (const tx of txs) {
realm.create(
@ -551,6 +568,7 @@ class AppStorage {
}
for (const index of Object.keys(walletToSave._txs_by_internal_index)) {
// @ts-ignore index is number
const txs = walletToSave._txs_by_internal_index[index];
for (const tx of txs) {
realm.create(
@ -576,7 +594,7 @@ class AppStorage {
*
* @returns {Promise} Result of storage save
*/
async saveToDisk() {
async saveToDisk(): Promise<void> {
if (savingInProgress) {
console.warn('saveToDisk is in progress');
if (++savingInProgress > 10) presentAlert({ message: 'Critical error. Last actions were not saved' }); // should never happen
@ -590,27 +608,29 @@ class AppStorage {
let realm;
try {
realm = await this.getRealm();
} catch (error) {
} catch (error: any) {
presentAlert({ message: error.message });
}
for (const key of this.wallets) {
if (typeof key === 'boolean') continue;
key.prepareForSerialization();
// @ts-ignore wtf is wallet.current? Does it even exist?
delete key.current;
const keyCloned = Object.assign({}, key); // stripped-down version of a wallet to save to secure keystore
if (key._hdWalletInstance) keyCloned._hdWalletInstance = Object.assign({}, key._hdWalletInstance);
if ('_hdWalletInstance' in key) {
const k = keyCloned as any & WatchOnlyWallet;
k._hdWalletInstance = Object.assign({}, key._hdWalletInstance);
k._hdWalletInstance._txs_by_external_index = {};
k._hdWalletInstance._txs_by_internal_index = {};
}
if (realm) this.offloadWalletToRealm(realm, key);
// stripping down:
if (key._txs_by_external_index) {
keyCloned._txs_by_external_index = {};
keyCloned._txs_by_internal_index = {};
}
if (key._hdWalletInstance) {
keyCloned._hdWalletInstance._txs_by_external_index = {};
keyCloned._hdWalletInstance._txs_by_internal_index = {};
}
if (keyCloned._bip47_instance) {
if ('_bip47_instance' in keyCloned) {
delete keyCloned._bip47_instance; // since it wont be restored into a proper class instance
}
@ -652,6 +672,7 @@ class AppStorage {
newData.push(encryption.encrypt(JSON.stringify(data), this.cachedPassword));
}
}
// @ts-ignore bla bla bla
data = newData;
}
@ -663,7 +684,7 @@ class AppStorage {
this.saveToRealmKeyValue(realmkeyValue, 'data', JSON.stringify(data));
this.saveToRealmKeyValue(realmkeyValue, AppStorage.FLAG_ENCRYPTED, this.cachedPassword ? '1' : '');
realmkeyValue.close();
} catch (error) {
} catch (error: any) {
console.error('save to disk exception:', error.message);
presentAlert({ message: 'save to disk exception: ' + error.message });
if (error.message.includes('Realm file decryption failed')) {
@ -680,10 +701,8 @@ class AppStorage {
* 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 => {
fetchWalletBalances = async (index?: number): Promise<void> => {
console.log('fetchWalletBalances for wallet#', typeof index === 'undefined' ? '(all)' : index);
if (index || index === 0) {
let c = 0;
@ -710,17 +729,16 @@ class AppStorage {
* blank to fetch from all wallets
* @return {Promise.<void>}
*/
fetchWalletTransactions = async index => {
fetchWalletTransactions = async (index?: number) => {
console.log('fetchWalletTransactions for wallet#', typeof index === 'undefined' ? '(all)' : index);
if (index || index === 0) {
let c = 0;
for (const wallet of this.wallets) {
if (c++ === index) {
await wallet.fetchTransactions();
if (wallet.fetchPendingTransactions) {
if ('fetchPendingTransactions' in wallet) {
await wallet.fetchPendingTransactions();
}
if (wallet.fetchUserInvoices) {
await wallet.fetchUserInvoices();
}
}
@ -728,29 +746,28 @@ class AppStorage {
} else {
for (const wallet of this.wallets) {
await wallet.fetchTransactions();
if (wallet.fetchPendingTransactions) {
if ('fetchPendingTransactions' in wallet) {
await wallet.fetchPendingTransactions();
}
if (wallet.fetchUserInvoices) {
await wallet.fetchUserInvoices();
}
}
}
};
fetchSenderPaymentCodes = async index => {
fetchSenderPaymentCodes = async (index?: number) => {
console.log('fetchSenderPaymentCodes for wallet#', typeof index === 'undefined' ? '(all)' : index);
if (index || index === 0) {
const wallet = this.wallets[index];
try {
if (!(this.wallets[index].allowBIP47() && this.wallets[index].isBIP47Enabled())) return;
await this.wallets[index].fetchBIP47SenderPaymentCodes();
if (!(wallet.allowBIP47() && wallet.isBIP47Enabled() && 'fetchBIP47SenderPaymentCodes' in wallet)) return;
await wallet.fetchBIP47SenderPaymentCodes();
} catch (error) {
console.error('Failed to fetch sender payment codes for wallet', index, error);
}
} else {
for (const wallet of this.wallets) {
try {
if (!(wallet.allowBIP47() && wallet.isBIP47Enabled())) continue;
if (!(wallet.allowBIP47() && wallet.isBIP47Enabled() && 'fetchBIP47SenderPaymentCodes' in wallet)) continue;
await wallet.fetchBIP47SenderPaymentCodes();
} catch (error) {
console.error('Failed to fetch sender payment codes for wallet', wallet.label, error);
@ -759,11 +776,7 @@ class AppStorage {
}
};
/**
*
* @returns {Array.<AbstractWallet>}
*/
getWallets = () => {
getWallets = (): TWallet[] => {
return this.wallets;
};
@ -774,11 +787,14 @@ class AppStorage {
* @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) => {
getTransactions = (
index?: number,
limit: number = Infinity,
includeWalletsWithHideTransactionsEnabled: boolean = false,
): Transaction[] => {
if (index || index === 0) {
let txs = [];
let txs: Transaction[] = [];
let c = 0;
for (const wallet of this.wallets) {
if (c++ === index) {
@ -788,7 +804,7 @@ class AppStorage {
return txs;
}
let txs = [];
let txs: Transaction[] = [];
for (const wallet of this.wallets.filter(w => includeWalletsWithHideTransactionsEnabled || !w.getHideTransactionsInWalletsList())) {
const walletTransactions = wallet.getTransactions();
const walletID = wallet.getID();
@ -799,23 +815,19 @@ class AppStorage {
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;
.sort((a, b) => {
const bTime = new Date(b.received!).getTime();
const aTime = new Date(a.received!).getTime();
return bTime - aTime;
})
.slice(0, limit);
};
/**
* Getter for a sum of all balances of all wallets
*
* @return {number}
*/
getBalance = () => {
getBalance = (): number => {
let finalBalance = 0;
for (const wal of this.wallets) {
finalBalance += wal.getBalance();
@ -823,48 +835,45 @@ class AppStorage {
return finalBalance;
};
isAdvancedModeEnabled = async () => {
isAdvancedModeEnabled = async (): Promise<boolean> => {
try {
return !!(await AsyncStorage.getItem(AppStorage.ADVANCED_MODE_ENABLED));
} catch (_) {}
return false;
};
setIsAdvancedModeEnabled = async value => {
setIsAdvancedModeEnabled = async (value: boolean) => {
await AsyncStorage.setItem(AppStorage.ADVANCED_MODE_ENABLED, value ? '1' : '');
};
isHandoffEnabled = async () => {
isHandoffEnabled = async (): Promise<boolean> => {
try {
return !!(await AsyncStorage.getItem(AppStorage.HANDOFF_STORAGE_KEY));
} catch (_) {}
return false;
};
setIsHandoffEnabled = async value => {
setIsHandoffEnabled = async (value: boolean): Promise<void> => {
await AsyncStorage.setItem(AppStorage.HANDOFF_STORAGE_KEY, value ? '1' : '');
};
isDoNotTrackEnabled = async () => {
isDoNotTrackEnabled = async (): Promise<boolean> => {
try {
return !!(await AsyncStorage.getItem(AppStorage.DO_NOT_TRACK));
} catch (_) {}
return false;
};
setDoNotTrack = async value => {
setDoNotTrack = async (value: string) => {
await AsyncStorage.setItem(AppStorage.DO_NOT_TRACK, value ? '1' : '');
await DefaultPreference.setName('group.io.bluewallet.bluewallet');
await DefaultPreference.set(AppStorage.DO_NOT_TRACK, value);
await DefaultPreference.set(AppStorage.DO_NOT_TRACK, value ? '1' : '');
};
/**
* Simple async sleeper function
*
* @param ms {number} Milliseconds to sleep
* @returns {Promise<Promise<*> | Promise<*>>}
*/
sleep = ms => {
sleep = (ms: number): Promise<void> => {
return new Promise(resolve => setTimeout(resolve, ms));
};
@ -880,14 +889,14 @@ const BlueApp = new AppStorage();
// If attempt reaches 10, a wipe keychain option will be provided to the user.
let unlockAttempt = 0;
const startAndDecrypt = async retry => {
export const startAndDecrypt = async (retry?: boolean): Promise<boolean> => {
console.log('startAndDecrypt');
if (BlueApp.getWallets().length > 0) {
console.log('App already has some wallets, so we are in already started state, exiting startAndDecrypt');
return true;
}
await BlueApp.migrateKeys();
let password = false;
let password: undefined | string;
if (await BlueApp.storageIsEncrypted()) {
do {
password = await prompt((retry && loc._.bad_password) || loc._.enter_password, loc._.storage_is_encrypted, false);
@ -938,8 +947,6 @@ const startAndDecrypt = async retry => {
}
};
BlueApp.startAndDecrypt = startAndDecrypt;
BlueApp.AppStorage = AppStorage;
initCurrencyDaemon();
module.exports = BlueApp;
export default BlueApp;

View File

@ -11,7 +11,6 @@ const getIsTorCapable = (): boolean => {
} else if (isDesktop) {
capable = false;
}
console.log('getIsTorCapable', capable);
return capable;
};

View File

@ -1,34 +1,96 @@
import React, { createContext, useEffect, useState } from 'react';
import { useAsyncStorage } from '@react-native-async-storage/async-storage';
import { FiatUnit } from '../models/fiatUnit';
import BlueApp, { TTXMetadata, startAndDecrypt } from '../BlueApp';
import Notifications from '../blue_modules/notifications';
import loc, { STORAGE_KEY as LOC_STORAGE_KEY } from '../loc';
import { LegacyWallet, WatchOnlyWallet } from '../class';
import type { TWallet } from '../class/wallets/types';
import presentAlert from '../components/Alert';
import triggerHapticFeedback, { HapticFeedbackTypes } from './hapticFeedback';
import loc, { STORAGE_KEY as LOC_STORAGE_KEY } from '../loc';
import { FiatUnit, TFiatUnit } from '../models/fiatUnit';
import { PREFERRED_CURRENCY_STORAGE_KEY } from './currency';
const BlueApp = require('../BlueApp');
import triggerHapticFeedback, { HapticFeedbackTypes } from './hapticFeedback';
const BlueElectrum = require('./BlueElectrum');
const A = require('../blue_modules/analytics');
const _lastTimeTriedToRefetchWallet = {}; // hashmap of timestamps we _started_ refetching some wallet
// hashmap of timestamps we _started_ refetching some wallet
const _lastTimeTriedToRefetchWallet: { [walletID: string]: number } = {};
export const WalletTransactionsStatus = { NONE: false, ALL: true };
export const BlueStorageContext = createContext();
export const BlueStorageProvider = ({ children }) => {
const [wallets, setWallets] = useState([]);
const [selectedWalletID, setSelectedWalletID] = useState();
const [walletTransactionUpdateStatus, setWalletTransactionUpdateStatus] = useState(WalletTransactionsStatus.NONE);
const [walletsInitialized, setWalletsInitialized] = useState(false);
const [preferredFiatCurrency, _setPreferredFiatCurrency] = useState(FiatUnit.USD);
const [language, _setLanguage] = useState();
interface BlueStorageContextType {
wallets: TWallet[];
setWalletsWithNewOrder: (wallets: TWallet[]) => void;
txMetadata: TTXMetadata;
saveToDisk: (force?: boolean) => Promise<void>;
selectedWalletID: string | undefined;
setSelectedWalletID: (walletID: string | undefined) => void;
addWallet: (wallet: TWallet) => void;
deleteWallet: (wallet: TWallet) => void;
currentSharedCosigner: string;
setSharedCosigner: (cosigner: string) => void;
addAndSaveWallet: (wallet: TWallet) => Promise<void>;
fetchAndSaveWalletTransactions: (walletID: string) => Promise<void>;
walletsInitialized: boolean;
setWalletsInitialized: (initialized: boolean) => void;
refreshAllWalletTransactions: (lastSnappedTo?: number, showUpdateStatusIndicator?: boolean) => Promise<void>;
resetWallets: () => void;
setPreferredFiatCurrency: () => void;
preferredFiatCurrency: TFiatUnit;
setLanguage: () => void;
language: string | undefined;
isHandOffUseEnabled: boolean;
setIsHandOffUseEnabledAsyncStorage: (value: boolean) => Promise<void>;
walletTransactionUpdateStatus: WalletTransactionsStatus | string;
setWalletTransactionUpdateStatus: (status: WalletTransactionsStatus | string) => void;
isElectrumDisabled: boolean;
setIsElectrumDisabled: (value: boolean) => void;
isPrivacyBlurEnabled: boolean;
setIsPrivacyBlurEnabled: (value: boolean) => void;
reloadTransactionsMenuActionFunction: () => void;
setReloadTransactionsMenuActionFunction: (func: () => void) => void;
getTransactions: typeof BlueApp.getTransactions;
isAdvancedModeEnabled: typeof BlueApp.isAdvancedModeEnabled;
fetchWalletBalances: typeof BlueApp.fetchWalletBalances;
fetchWalletTransactions: typeof BlueApp.fetchWalletTransactions;
getBalance: typeof BlueApp.getBalance;
isStorageEncrypted: typeof BlueApp.storageIsEncrypted;
startAndDecrypt: typeof startAndDecrypt;
encryptStorage: typeof BlueApp.encryptStorage;
sleep: typeof BlueApp.sleep;
createFakeStorage: typeof BlueApp.createFakeStorage;
decryptStorage: typeof BlueApp.decryptStorage;
isPasswordInUse: typeof BlueApp.isPasswordInUse;
cachedPassword: typeof BlueApp.cachedPassword;
setIsAdvancedModeEnabled: typeof BlueApp.setIsAdvancedModeEnabled;
setDoNotTrack: typeof BlueApp.setDoNotTrack;
isDoNotTrackEnabled: typeof BlueApp.isDoNotTrackEnabled;
getItem: typeof BlueApp.getItem;
setItem: typeof BlueApp.setItem;
}
export enum WalletTransactionsStatus {
NONE = 'NONE',
ALL = 'ALL',
}
// @ts-ignore defaut value does not match the type
export const BlueStorageContext = createContext<BlueStorageContextType>(undefined);
export const BlueStorageProvider = ({ children }: { children: React.ReactNode }) => {
const [wallets, setWallets] = useState<TWallet[]>([]);
const [selectedWalletID, setSelectedWalletID] = useState<undefined | string>();
const [walletTransactionUpdateStatus, setWalletTransactionUpdateStatus] = useState<WalletTransactionsStatus | string>(
WalletTransactionsStatus.NONE,
);
const [walletsInitialized, setWalletsInitialized] = useState<boolean>(false);
const [preferredFiatCurrency, _setPreferredFiatCurrency] = useState<TFiatUnit>(FiatUnit.USD);
const [language, _setLanguage] = useState<string | undefined>();
const [isHandOffUseEnabled, setIsHandOffUseEnabled] = useState<boolean>(false);
const [isElectrumDisabled, setIsElectrumDisabled] = useState<boolean>(true);
const [isPrivacyBlurEnabled, setIsPrivacyBlurEnabled] = useState<boolean>(true);
const [currentSharedCosigner, setCurrentSharedCosigner] = useState<string>('');
const getPreferredCurrencyAsyncStorage = useAsyncStorage(PREFERRED_CURRENCY_STORAGE_KEY).getItem;
const getLanguageAsyncStorage = useAsyncStorage(LOC_STORAGE_KEY).getItem;
const [isHandOffUseEnabled, setIsHandOffUseEnabled] = useState(false);
const [isElectrumDisabled, setIsElectrumDisabled] = useState(true);
const [isPrivacyBlurEnabled, setIsPrivacyBlurEnabled] = useState(true);
const [currentSharedCosigner, setCurrentSharedCosigner] = useState('');
const [reloadTransactionsMenuActionFunction, setReloadTransactionsMenuActionFunction] = useState(() => {});
const [reloadTransactionsMenuActionFunction, setReloadTransactionsMenuActionFunction] = useState<() => void>(() => {});
useEffect(() => {
BlueElectrum.isDisabled().then(setIsElectrumDisabled);
@ -47,12 +109,12 @@ export const BlueStorageProvider = ({ children }) => {
}
}, [isPrivacyBlurEnabled]);
const setIsHandOffUseEnabledAsyncStorage = value => {
const setIsHandOffUseEnabledAsyncStorage = (value: boolean) => {
setIsHandOffUseEnabled(value);
return BlueApp.setIsHandoffEnabled(value);
};
const saveToDisk = async (force = false) => {
const saveToDisk = async (force: boolean = false) => {
if (BlueApp.getWallets().length === 0 && !force) {
console.log('not saving empty wallets array');
return;
@ -80,6 +142,7 @@ export const BlueStorageProvider = ({ children }) => {
}, []);
const getPreferredCurrency = async () => {
// @ts-ignore TODO: fix this
const item = JSON.parse(await getPreferredCurrencyAsyncStorage()) ?? FiatUnit.USD;
_setPreferredFiatCurrency(item);
return item;
@ -91,6 +154,9 @@ export const BlueStorageProvider = ({ children }) => {
const getLanguage = async () => {
const item = await getLanguageAsyncStorage();
if (item === null) {
return;
}
_setLanguage(item);
};
@ -108,12 +174,12 @@ export const BlueStorageProvider = ({ children }) => {
setWallets(BlueApp.getWallets());
};
const setWalletsWithNewOrder = wlts => {
const setWalletsWithNewOrder = (wlts: TWallet[]) => {
BlueApp.wallets = wlts;
saveToDisk();
};
const refreshAllWalletTransactions = async (lastSnappedTo, showUpdateStatusIndicator = true) => {
const refreshAllWalletTransactions = async (lastSnappedTo?: number, showUpdateStatusIndicator: boolean = true) => {
let noErr = true;
try {
await BlueElectrum.waitTillConnected();
@ -121,7 +187,7 @@ export const BlueStorageProvider = ({ children }) => {
setWalletTransactionUpdateStatus(WalletTransactionsStatus.ALL);
}
const paymentCodesStart = Date.now();
await fetchSenderPaymentCodes(lastSnappedTo);
await BlueApp.fetchSenderPaymentCodes(lastSnappedTo);
const paymentCodesEnd = Date.now();
console.log('fetch payment codes took', (paymentCodesEnd - paymentCodesStart) / 1000, 'sec');
const balanceStart = +new Date();
@ -141,7 +207,7 @@ export const BlueStorageProvider = ({ children }) => {
if (noErr) await saveToDisk(); // caching
};
const fetchAndSaveWalletTransactions = async walletID => {
const fetchAndSaveWalletTransactions = async (walletID: string) => {
const index = wallets.findIndex(wallet => wallet.getID() === walletID);
let noErr = true;
try {
@ -171,17 +237,17 @@ export const BlueStorageProvider = ({ children }) => {
if (noErr) await saveToDisk(); // caching
};
const addWallet = wallet => {
const addWallet = (wallet: TWallet) => {
BlueApp.wallets.push(wallet);
setWallets([...BlueApp.getWallets()]);
};
const deleteWallet = wallet => {
const deleteWallet = (wallet: TWallet) => {
BlueApp.deleteWallet(wallet);
setWallets([...BlueApp.getWallets()]);
};
const addAndSaveWallet = async w => {
const addAndSaveWallet = async (w: TWallet) => {
if (wallets.some(i => i.getID() === w.getID())) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: 'This wallet has been previously imported.' });
@ -195,26 +261,20 @@ export const BlueStorageProvider = ({ children }) => {
await saveToDisk();
A(A.ENUM.CREATED_WALLET);
presentAlert({ message: w.type === WatchOnlyWallet.type ? loc.wallets.import_success_watchonly : loc.wallets.import_success });
// @ts-ignore need to type notifications first
Notifications.majorTomToGroundControl(w.getAllExternalAddresses(), [], []);
// start balance fetching at the background
await w.fetchBalance();
setWallets([...BlueApp.getWallets()]);
};
const setSharedCosigner = cosigner => {
setCurrentSharedCosigner(cosigner);
};
let txMetadata = BlueApp.tx_metadata || {};
let txMetadata = BlueApp.tx_metadata;
const getTransactions = BlueApp.getTransactions;
const isAdvancedModeEnabled = BlueApp.isAdvancedModeEnabled;
const fetchSenderPaymentCodes = BlueApp.fetchSenderPaymentCodes;
const fetchWalletBalances = BlueApp.fetchWalletBalances;
const fetchWalletTransactions = BlueApp.fetchWalletTransactions;
const getBalance = BlueApp.getBalance;
const isStorageEncrypted = BlueApp.storageIsEncrypted;
const startAndDecrypt = BlueApp.startAndDecrypt;
const encryptStorage = BlueApp.encryptStorage;
const sleep = BlueApp.sleep;
const createFakeStorage = BlueApp.createFakeStorage;
@ -227,60 +287,56 @@ export const BlueStorageProvider = ({ children }) => {
const getItem = BlueApp.getItem;
const setItem = BlueApp.setItem;
return (
<BlueStorageContext.Provider
value={{
wallets,
setWalletsWithNewOrder,
txMetadata,
saveToDisk,
getTransactions,
selectedWalletID,
setSelectedWalletID,
addWallet,
deleteWallet,
currentSharedCosigner,
setSharedCosigner,
addAndSaveWallet,
setItem,
getItem,
isAdvancedModeEnabled,
fetchWalletBalances,
fetchWalletTransactions,
fetchAndSaveWalletTransactions,
isStorageEncrypted,
encryptStorage,
startAndDecrypt,
cachedPassword,
getBalance,
walletsInitialized,
setWalletsInitialized,
refreshAllWalletTransactions,
sleep,
createFakeStorage,
resetWallets,
decryptStorage,
isPasswordInUse,
setIsAdvancedModeEnabled,
setPreferredFiatCurrency,
preferredFiatCurrency,
setLanguage,
language,
isHandOffUseEnabled,
setIsHandOffUseEnabledAsyncStorage,
walletTransactionUpdateStatus,
setWalletTransactionUpdateStatus,
setDoNotTrack,
isDoNotTrackEnabled,
isElectrumDisabled,
setIsElectrumDisabled,
isPrivacyBlurEnabled,
setIsPrivacyBlurEnabled,
reloadTransactionsMenuActionFunction,
setReloadTransactionsMenuActionFunction,
}}
>
{children}
</BlueStorageContext.Provider>
);
const value: BlueStorageContextType = {
wallets,
setWalletsWithNewOrder,
txMetadata,
saveToDisk,
getTransactions,
selectedWalletID,
setSelectedWalletID,
addWallet,
deleteWallet,
currentSharedCosigner,
setSharedCosigner: setCurrentSharedCosigner,
addAndSaveWallet,
setItem,
getItem,
isAdvancedModeEnabled,
fetchWalletBalances,
fetchWalletTransactions,
fetchAndSaveWalletTransactions,
isStorageEncrypted,
encryptStorage,
startAndDecrypt,
cachedPassword,
getBalance,
walletsInitialized,
setWalletsInitialized,
refreshAllWalletTransactions,
sleep,
createFakeStorage,
resetWallets,
decryptStorage,
isPasswordInUse,
setIsAdvancedModeEnabled,
setPreferredFiatCurrency,
preferredFiatCurrency,
setLanguage,
language,
isHandOffUseEnabled,
setIsHandOffUseEnabledAsyncStorage,
walletTransactionUpdateStatus,
setWalletTransactionUpdateStatus,
setDoNotTrack,
isDoNotTrackEnabled,
isElectrumDisabled,
setIsElectrumDisabled,
isPrivacyBlurEnabled,
setIsPrivacyBlurEnabled,
reloadTransactionsMenuActionFunction,
setReloadTransactionsMenuActionFunction,
};
return <BlueStorageContext.Provider value={value}>{children}</BlueStorageContext.Provider>;
};

View File

@ -1,6 +1,6 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { AbstractWallet } from './wallets/abstract-wallet';
const BlueApp = require('../BlueApp');
import { TWallet } from './wallets/types';
import BlueApp from '../BlueApp';
export default class OnAppLaunch {
static STORAGE_KEY = 'ONAPP_LAUNCH_SELECTED_DEFAULT_WALLET_KEY';
@ -26,18 +26,18 @@ export default class OnAppLaunch {
}
}
static async getSelectedDefaultWallet(): Promise<AbstractWallet | boolean> {
let selectedWallet: AbstractWallet | false = false;
static async getSelectedDefaultWallet(): Promise<TWallet | undefined> {
let selectedWallet: TWallet | undefined;
try {
const selectedWalletID = JSON.parse((await AsyncStorage.getItem(OnAppLaunch.STORAGE_KEY)) || 'null');
if (selectedWalletID) {
selectedWallet = BlueApp.getWallets().find((wallet: AbstractWallet) => wallet.getID() === selectedWalletID);
selectedWallet = BlueApp.getWallets().find(wallet => wallet.getID() === selectedWalletID);
if (!selectedWallet) {
await AsyncStorage.setItem(OnAppLaunch.STORAGE_KEY, '');
}
}
} catch (_e) {
return false;
return;
}
return selectedWallet;
}

View File

@ -1,14 +1,15 @@
import React, { useContext, useEffect } from 'react';
import QuickActions from 'react-native-quick-actions';
import { DeviceEventEmitter, Linking, Platform } from 'react-native';
import { formatBalance } from '../loc';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { CommonActions } from '@react-navigation/native';
import { DeviceEventEmitter, Linking, Platform } from 'react-native';
import QuickActions from 'react-native-quick-actions';
import * as NavigationService from '../NavigationService';
import { BlueStorageContext } from '../blue_modules/storage-context';
import { formatBalance } from '../loc';
import DeeplinkSchemaMatch from './deeplink-schema-match';
import OnAppLaunch from './on-app-launch';
import * as NavigationService from '../NavigationService';
import { CommonActions } from '@react-navigation/native';
import { AbstractWallet } from './wallets/abstract-wallet';
import { TWallet } from './wallets/types';
const DeviceQuickActionsStorageKey = 'DeviceQuickActionsEnabled';
@ -93,9 +94,9 @@ function DeviceQuickActions(): JSX.Element | null {
} else {
const isViewAllWalletsEnabled = await OnAppLaunch.isViewAllWalletsEnabled();
if (!isViewAllWalletsEnabled) {
const selectedDefaultWallet: AbstractWallet = (await OnAppLaunch.getSelectedDefaultWallet()) as AbstractWallet;
const selectedDefaultWallet = (await OnAppLaunch.getSelectedDefaultWallet()) as TWallet;
if (selectedDefaultWallet) {
const wallet = wallets.find((w: AbstractWallet) => w.getID() === selectedDefaultWallet.getID());
const wallet = wallets.find(w => w.getID() === selectedDefaultWallet.getID());
if (wallet) {
NavigationService.dispatch(
CommonActions.navigate({

View File

@ -31,10 +31,14 @@ type BalanceByIndex = {
* Electrum - means that it utilizes Electrum protocol for blockchain data
*/
export class AbstractHDElectrumWallet extends AbstractHDWallet {
static type = 'abstract';
static typeReadable = 'abstract';
static readonly type = 'abstract';
static readonly typeReadable = 'abstract';
static defaultRBFSequence = 2147483648; // 1 << 31, minimum for replaceable transactions as per BIP68
static finalRBFSequence = 4294967295; // 0xFFFFFFFF
// @ts-ignore: override
public readonly type = AbstractHDElectrumWallet.type;
// @ts-ignore: override
public readonly typeReadable = AbstractHDElectrumWallet.typeReadable;
_balances_by_external_index: Record<number, BalanceByIndex>;
_balances_by_internal_index: Record<number, BalanceByIndex>;

View File

@ -13,8 +13,12 @@ type AbstractHDWalletStatics = {
* @deprecated
*/
export class AbstractHDWallet extends LegacyWallet {
static type = 'abstract';
static typeReadable = 'abstract';
static readonly type = 'abstract';
static readonly typeReadable = 'abstract';
// @ts-ignore: override
public readonly type = AbstractHDWallet.type;
// @ts-ignore: override
public readonly typeReadable = AbstractHDWallet.typeReadable;
next_free_address_index: number;
next_free_change_address_index: number;

View File

@ -1,15 +1,8 @@
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import b58 from 'bs58check';
import createHash from 'create-hash';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import { CreateTransactionResult, CreateTransactionUtxo, Transaction, Utxo } from './types';
type WalletStatics = {
type: string;
typeReadable: string;
segwitType?: 'p2wpkh' | 'p2sh(p2wpkh)';
derivationPath?: string;
};
type WalletWithPassphrase = AbstractWallet & { getPassphrase: () => string };
type UtxoMetadata = {
frozen?: boolean;
@ -17,8 +10,12 @@ type UtxoMetadata = {
};
export class AbstractWallet {
static type = 'abstract';
static typeReadable = 'abstract';
static readonly type = 'abstract';
static readonly typeReadable = 'abstract';
// @ts-ignore: override
public readonly type = AbstractWallet.type;
// @ts-ignore: override
public readonly typeReadable = AbstractWallet.typeReadable;
static fromJson(obj: string): AbstractWallet {
const obj2 = JSON.parse(obj);
@ -31,8 +28,6 @@ export class AbstractWallet {
return temp;
}
type: string;
typeReadable: string;
segwitType?: 'p2wpkh' | 'p2sh(p2wpkh)';
_derivationPath?: string;
label: string;
@ -50,14 +45,9 @@ export class AbstractWallet {
_hideTransactionsInWalletsList: boolean;
_utxoMetadata: Record<string, UtxoMetadata>;
use_with_hardware_wallet: boolean;
masterFingerprint: number | false;
masterFingerprint: number;
constructor() {
const Constructor = this.constructor as unknown as WalletStatics;
this.type = Constructor.type;
this.typeReadable = Constructor.typeReadable;
this.segwitType = Constructor.segwitType;
this.label = '';
this.secret = ''; // private key or recovery phrase
this.balance = 0;
@ -73,7 +63,7 @@ export class AbstractWallet {
this._hideTransactionsInWalletsList = false;
this._utxoMetadata = {};
this.use_with_hardware_wallet = false;
this.masterFingerprint = false;
this.masterFingerprint = 0;
}
/**
@ -257,7 +247,7 @@ export class AbstractWallet {
parsedSecret = JSON.parse(newSecret);
}
if (parsedSecret && parsedSecret.keystore && parsedSecret.keystore.xpub) {
let masterFingerprint: number | false = false;
let masterFingerprint: number = 0;
if (parsedSecret.keystore.ckcc_xfp) {
// It is a ColdCard Hardware Wallet
masterFingerprint = Number(parsedSecret.keystore.ckcc_xfp);

View File

@ -18,10 +18,14 @@ const bip32 = BIP32Factory(ecc);
* @see https://github.com/lightningnetwork/lnd/blob/master/keychain/derivation.go
*/
export class HDAezeedWallet extends AbstractHDElectrumWallet {
static type = 'HDAezeedWallet';
static typeReadable = 'HD Aezeed';
static segwitType = 'p2wpkh';
static derivationPath = "m/84'/0'/0'";
static readonly type = 'HDAezeedWallet';
static readonly typeReadable = 'HD Aezeed';
public readonly segwitType = 'p2wpkh';
static readonly derivationPath = "m/84'/0'/0'";
// @ts-ignore: override
public readonly type = HDAezeedWallet.type;
// @ts-ignore: override
public readonly typeReadable = HDAezeedWallet.typeReadable;
private _entropyHex?: string;

View File

@ -1,10 +1,12 @@
import BIP32Factory, { BIP32Interface } from 'bip32';
import * as bitcoinjs from 'bitcoinjs-lib';
import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet';
import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet';
import BIP32Factory from 'bip32';
import { Psbt } from 'bitcoinjs-lib';
import { CoinSelectReturnInput } from 'coinselect';
import BlueElectrum, { ElectrumHistory } from '../../blue_modules/BlueElectrum';
import ecc from '../../blue_modules/noble_ecc';
import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet';
import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet';
const BlueElectrum = require('../../blue_modules/BlueElectrum');
const bip32 = BIP32Factory(ecc);
/**
@ -12,32 +14,46 @@ const bip32 = BIP32Factory(ecc);
* In particular, Breadwallet-compatible (Legacy addresses)
*/
export class HDLegacyBreadwalletWallet extends HDLegacyP2PKHWallet {
static type = 'HDLegacyBreadwallet';
static typeReadable = 'HD Legacy Breadwallet (P2PKH)';
static derivationPath = "m/0'";
static readonly type = 'HDLegacyBreadwallet';
static readonly typeReadable = 'HD Legacy Breadwallet (P2PKH)';
// @ts-ignore: override
public readonly type = HDLegacyBreadwalletWallet.type;
// @ts-ignore: override
public readonly typeReadable = HDLegacyBreadwalletWallet.typeReadable;
static readonly derivationPath = "m/0'";
// track address index at which wallet switched to segwit
_external_segwit_index = null;
_internal_segwit_index = null;
_external_segwit_index: number | null = null;
_internal_segwit_index: number | null = null;
// we need a separate function without external_addresses_cache to use in binarySearch
_calcNodeAddressByIndex(node, index, p2wpkh = false) {
let _node;
_calcNodeAddressByIndex(node: number, index: number, p2wpkh: boolean = false) {
let _node: BIP32Interface | undefined;
if (node === 0) {
_node = this._node0 || (this._node0 = bip32.fromBase58(this.getXpub()).derive(node));
}
if (node === 1) {
_node = this._node1 || (this._node1 = bip32.fromBase58(this.getXpub()).derive(node));
}
if (!_node) {
throw new Error('Internal error: this._node0 or this._node1 is undefined');
}
const pubkey = _node.derive(index).publicKey;
const address = p2wpkh ? bitcoinjs.payments.p2wpkh({ pubkey }).address : bitcoinjs.payments.p2pkh({ pubkey }).address;
if (!address) {
throw new Error('Internal error: no address in _calcNodeAddressByIndex');
}
return address;
}
// this function is different from HDLegacyP2PKHWallet._getNodeAddressByIndex.
// It takes _external_segwit_index _internal_segwit_index for account
// and starts to generate segwit addresses if index more than them
_getNodeAddressByIndex(node, index) {
_getNodeAddressByIndex(node: number, index: number): string {
index = index * 1; // cast to int
if (node === 0) {
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
@ -64,6 +80,8 @@ export class HDLegacyBreadwalletWallet extends HDLegacyP2PKHWallet {
if (node === 1) {
return (this.internal_addresses_cache[index] = address);
}
throw new Error('Internal error: unknown node');
}
async fetchBalance() {
@ -96,8 +114,8 @@ export class HDLegacyBreadwalletWallet extends HDLegacyP2PKHWallet {
}
}
async _binarySearchIteration(startIndex, endIndex, node = 0, p2wpkh = false) {
const gerenateChunkAddresses = chunkNum => {
async _binarySearchIteration(startIndex: number, endIndex: number, node: number = 0, p2wpkh: boolean = false) {
const gerenateChunkAddresses = (chunkNum: number) => {
const ret = [];
for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) {
ret.push(this._calcNodeAddressByIndex(node, c, p2wpkh));
@ -105,11 +123,11 @@ export class HDLegacyBreadwalletWallet extends HDLegacyP2PKHWallet {
return ret;
};
let lastChunkWithUsedAddressesNum = null;
let lastHistoriesWithUsedAddresses = null;
let lastChunkWithUsedAddressesNum: number;
let lastHistoriesWithUsedAddresses: Record<string, ElectrumHistory[]>;
for (let c = 0; c < Math.round(endIndex / this.gap_limit); c++) {
const histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c));
if (this.constructor._getTransactionsFromHistories(histories).length > 0) {
if (AbstractHDElectrumWallet._getTransactionsFromHistories(histories).length > 0) {
// in this particular chunk we have used addresses
lastChunkWithUsedAddressesNum = c;
lastHistoriesWithUsedAddresses = histories;
@ -121,11 +139,11 @@ export class HDLegacyBreadwalletWallet extends HDLegacyP2PKHWallet {
let lastUsedIndex = startIndex;
if (lastHistoriesWithUsedAddresses) {
if (lastHistoriesWithUsedAddresses!) {
// now searching for last used address in batch lastChunkWithUsedAddressesNum
for (
let c = lastChunkWithUsedAddressesNum * this.gap_limit;
c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit;
let c = lastChunkWithUsedAddressesNum! * this.gap_limit;
c < lastChunkWithUsedAddressesNum! * this.gap_limit + this.gap_limit;
c++
) {
const address = this._calcNodeAddressByIndex(node, c, p2wpkh);
@ -138,11 +156,11 @@ export class HDLegacyBreadwalletWallet extends HDLegacyP2PKHWallet {
return lastUsedIndex;
}
_addPsbtInput(psbt, input, sequence, masterFingerprintBuffer) {
_addPsbtInput(psbt: Psbt, input: CoinSelectReturnInput, sequence: number, masterFingerprintBuffer: Buffer) {
// hack to use
// AbstractHDElectrumWallet._addPsbtInput for bech32 address
// HDLegacyP2PKHWallet._addPsbtInput for legacy address
const ProxyClass = input.address.startsWith('bc1') ? AbstractHDElectrumWallet : HDLegacyP2PKHWallet;
const ProxyClass = input?.address?.startsWith('bc1') ? AbstractHDElectrumWallet : HDLegacyP2PKHWallet;
const proxy = new ProxyClass();
return proxy._addPsbtInput.apply(this, [psbt, input, sequence, masterFingerprintBuffer]);
}

View File

@ -19,9 +19,13 @@ type SeedOpts = {
* @see https://electrum.readthedocs.io/en/latest/seedphrase.html
*/
export class HDLegacyElectrumSeedP2PKHWallet extends HDLegacyP2PKHWallet {
static type = 'HDlegacyElectrumSeedP2PKH';
static typeReadable = 'HD Legacy Electrum (BIP32 P2PKH)';
static derivationPath = 'm';
static readonly type = 'HDlegacyElectrumSeedP2PKH';
static readonly typeReadable = 'HD Legacy Electrum (BIP32 P2PKH)';
// @ts-ignore: override
public readonly type = HDLegacyElectrumSeedP2PKHWallet.type;
// @ts-ignore: override
public readonly typeReadable = HDLegacyElectrumSeedP2PKHWallet.typeReadable;
static readonly derivationPath = 'm';
validateMnemonic() {
return mn.validateMnemonic(this.secret, PREFIX);

View File

@ -12,9 +12,13 @@ const BlueElectrum = require('../../blue_modules/BlueElectrum');
* @see https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
*/
export class HDLegacyP2PKHWallet extends AbstractHDElectrumWallet {
static type = 'HDlegacyP2PKH';
static typeReadable = 'HD Legacy (BIP44 P2PKH)';
static derivationPath = "m/44'/0'/0'";
static readonly type = 'HDlegacyP2PKH';
static readonly typeReadable = 'HD Legacy (BIP44 P2PKH)';
// @ts-ignore: override
public readonly type = HDLegacyP2PKHWallet.type;
// @ts-ignore: override
public readonly typeReadable = HDLegacyP2PKHWallet.typeReadable;
static readonly derivationPath = "m/44'/0'/0'";
allowSend() {
return true;

View File

@ -6,10 +6,14 @@ import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet';
* @see https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki
*/
export class HDSegwitBech32Wallet extends AbstractHDElectrumWallet {
static type = 'HDsegwitBech32';
static typeReadable = 'HD SegWit (BIP84 Bech32 Native)';
static segwitType = 'p2wpkh';
static derivationPath = "m/84'/0'/0'";
static readonly type = 'HDsegwitBech32';
static readonly typeReadable = 'HD SegWit (BIP84 Bech32 Native)';
// @ts-ignore: override
public readonly type = HDSegwitBech32Wallet.type;
// @ts-ignore: override
public readonly typeReadable = HDSegwitBech32Wallet.typeReadable;
public readonly segwitType = 'p2wpkh';
static readonly derivationPath = "m/84'/0'/0'";
allowSend() {
return true;

View File

@ -20,9 +20,13 @@ type SeedOpts = {
* @see https://electrum.readthedocs.io/en/latest/seedphrase.html
*/
export class HDSegwitElectrumSeedP2WPKHWallet extends HDSegwitBech32Wallet {
static type = 'HDSegwitElectrumSeedP2WPKHWallet';
static typeReadable = 'HD Electrum (BIP32 P2WPKH)';
static derivationPath = "m/0'";
static readonly type = 'HDSegwitElectrumSeedP2WPKHWallet';
static readonly typeReadable = 'HD Electrum (BIP32 P2WPKH)';
// @ts-ignore: override
public readonly type = HDSegwitElectrumSeedP2WPKHWallet.type;
// @ts-ignore: override
public readonly typeReadable = HDSegwitElectrumSeedP2WPKHWallet.typeReadable;
static readonly derivationPath = "m/0'";
validateMnemonic() {
return mn.validateMnemonic(this.secret, PREFIX);

View File

@ -14,10 +14,14 @@ const bip32 = BIP32Factory(ecc);
* @see https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki
*/
export class HDSegwitP2SHWallet extends AbstractHDElectrumWallet {
static type = 'HDsegwitP2SH';
static typeReadable = 'HD SegWit (BIP49 P2SH)';
static segwitType = 'p2sh(p2wpkh)';
static derivationPath = "m/49'/0'/0'";
static readonly type = 'HDsegwitP2SH';
static readonly typeReadable = 'HD SegWit (BIP49 P2SH)';
// @ts-ignore: override
public readonly type = HDSegwitP2SHWallet.type;
// @ts-ignore: override
public readonly typeReadable = HDSegwitP2SHWallet.typeReadable;
public readonly segwitType = 'p2sh(p2wpkh)';
static readonly derivationPath = "m/49'/0'/0'";
allowSend() {
return true;

View File

@ -19,8 +19,12 @@ bitcoin.initEccLib(ecc);
* (legacy P2PKH compressed)
*/
export class LegacyWallet extends AbstractWallet {
static type = 'legacy';
static typeReadable = 'Legacy (P2PKH)';
static readonly type = 'legacy';
static readonly typeReadable = 'Legacy (P2PKH)';
// @ts-ignore: override
public readonly type = LegacyWallet.type;
// @ts-ignore: override
public readonly typeReadable = LegacyWallet.typeReadable;
_txs_by_external_index: Transaction[] = [];
_txs_by_internal_index: Transaction[] = [];

View File

@ -4,23 +4,32 @@ import bolt11 from 'bolt11';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
export class LightningCustodianWallet extends LegacyWallet {
static type = 'lightningCustodianWallet';
static typeReadable = 'Lightning';
static readonly type = 'lightningCustodianWallet';
static readonly typeReadable = 'Lightning';
// @ts-ignore: override
public readonly type = LightningCustodianWallet.type;
// @ts-ignore: override
public readonly typeReadable = LightningCustodianWallet.typeReadable;
constructor(props) {
super(props);
this.setBaseURI(); // no args to init with default value
baseURI?: string;
refresh_token: string = '';
access_token: string = '';
_refresh_token_created_ts: number = 0;
_access_token_created_ts: number = 0;
refill_addressess: string[] = [];
pending_transactions_raw: any[] = [];
transactions_raw: any[] = [];
user_invoices_raw: any[] = [];
info_raw = false;
preferredBalanceUnit = BitcoinUnit.SATS;
chain = Chain.OFFCHAIN;
private _api?: Frisbee;
last_paid_invoice_result?: any;
decoded_invoice_raw?: any;
constructor() {
super();
this.init();
this.refresh_token = '';
this.access_token = '';
this._refresh_token_created_ts = 0;
this._access_token_created_ts = 0;
this.refill_addressess = [];
this.pending_transactions_raw = [];
this.user_invoices_raw = [];
this.info_raw = false;
this.preferredBalanceUnit = BitcoinUnit.SATS;
this.chain = Chain.OFFCHAIN;
}
/**
@ -28,7 +37,7 @@ export class LightningCustodianWallet extends LegacyWallet {
*
* @param URI
*/
setBaseURI(URI) {
setBaseURI(URI: string | undefined) {
this.baseURI = URI;
}
@ -40,11 +49,11 @@ export class LightningCustodianWallet extends LegacyWallet {
return true;
}
getAddress() {
getAddress(): string | false {
if (this.refill_addressess.length > 0) {
return this.refill_addressess[0];
} else {
return undefined;
return false;
}
}
@ -60,8 +69,9 @@ export class LightningCustodianWallet extends LegacyWallet {
return (+new Date() - this._lastTxFetch) / 1000 > 300; // 5 min
}
static fromJson(param) {
static fromJson(param: any) {
const obj = super.fromJson(param);
// @ts-ignore: local init
obj.init();
return obj;
}
@ -84,11 +94,13 @@ export class LightningCustodianWallet extends LegacyWallet {
return (+new Date() - this._refresh_token_created_ts) / 1000 >= 3600 * 24 * 7; // 7d
}
generate() {
generate(): Promise<void> {
// nop
return Promise.resolve();
}
async createAccount(isTest) {
async createAccount(isTest: boolean = false) {
if (!this._api) throw new Error('Internal error: _api is not initialized');
const response = await this._api.post('/create', {
body: { partnerid: 'bluewallet', accounttype: (isTest && 'test') || 'common' },
headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' },
@ -109,7 +121,8 @@ export class LightningCustodianWallet extends LegacyWallet {
this.secret = 'lndhub://' + json.login + ':' + json.password;
}
async payInvoice(invoice, freeAmount = 0) {
async payInvoice(invoice: string, freeAmount: number = 0) {
if (!this._api) throw new Error('Internal error: _api is not initialized');
const response = await this._api.post('/payinvoice', {
body: { invoice, amount: freeAmount },
headers: {
@ -146,9 +159,10 @@ export class LightningCustodianWallet extends LegacyWallet {
*
* @return {Promise.<Array>}
*/
async getUserInvoices(limit = false) {
async getUserInvoices(limit: number | false = false) {
if (!this._api) throw new Error('Internal error: _api is not initialized');
let limitString = '';
if (limit) limitString = '?limit=' + parseInt(limit, 10);
if (limit) limitString = '?limit=' + parseInt(limit as unknown as string, 10);
const response = await this._api.get('/getuserinvoices' + limitString, {
headers: {
'Access-Control-Allow-Origin': '*',
@ -184,7 +198,7 @@ export class LightningCustodianWallet extends LegacyWallet {
}
}
this.user_invoices_raw = json.sort(function (a, b) {
this.user_invoices_raw = json.sort(function (a: { timestamp: number }, b: { timestamp: number }) {
return a.timestamp - b.timestamp;
});
@ -201,15 +215,16 @@ export class LightningCustodianWallet extends LegacyWallet {
await this.getUserInvoices();
}
isInvoiceGeneratedByWallet(paymentRequest) {
isInvoiceGeneratedByWallet(paymentRequest: string) {
return this.user_invoices_raw.some(invoice => invoice.payment_request === paymentRequest);
}
weOwnAddress(address) {
weOwnAddress(address: string) {
return this.refill_addressess.some(refillAddress => address === refillAddress);
}
async addInvoice(amt, memo) {
async addInvoice(amt: number, memo: string) {
if (!this._api) throw new Error('Internal error: _api is not initialized');
const response = await this._api.post('/addinvoice', {
body: { amt: amt + '', memo },
headers: {
@ -241,6 +256,7 @@ export class LightningCustodianWallet extends LegacyWallet {
* @return {Promise.<void>}
*/
async authorize() {
if (!this._api) throw new Error('Internal error: _api is not initialized');
let login, password;
if (this.secret.indexOf('blitzhub://') !== -1) {
login = this.secret.replace('blitzhub://', '').split(':')[0];
@ -296,6 +312,7 @@ export class LightningCustodianWallet extends LegacyWallet {
}
async refreshAcessToken() {
if (!this._api) throw new Error('Internal error: _api is not initialized');
const response = await this._api.post('/auth?type=refresh_token', {
body: { refresh_token: this.refresh_token },
headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' },
@ -321,6 +338,7 @@ export class LightningCustodianWallet extends LegacyWallet {
}
async fetchBtcAddress() {
if (!this._api) throw new Error('Internal error: _api is not initialized');
const response = await this._api.get('/getbtc', {
headers: {
'Access-Control-Allow-Origin': '*',
@ -360,12 +378,9 @@ export class LightningCustodianWallet extends LegacyWallet {
}
getTransactions() {
let txs = [];
this.pending_transactions_raw = this.pending_transactions_raw || [];
this.user_invoices_raw = this.user_invoices_raw || [];
this.transactions_raw = this.transactions_raw || [];
let txs: any = [];
txs = txs.concat(this.pending_transactions_raw.slice(), this.transactions_raw.slice().reverse(), this.user_invoices_raw.slice()); // slice so array is cloned
// transforming to how wallets/list screen expects it
for (const tx of txs) {
tx.walletID = this.getID();
if (tx.amount) {
@ -378,6 +393,7 @@ export class LightningCustodianWallet extends LegacyWallet {
if (typeof tx.amt !== 'undefined' && typeof tx.fee !== 'undefined') {
// lnd tx outgoing
// @ts-ignore: fixme wtf?
tx.value = parseInt((tx.amt * 1 + tx.fee * 1) * -1, 10);
}
@ -399,12 +415,13 @@ export class LightningCustodianWallet extends LegacyWallet {
tx.received = new Date(tx.timestamp * 1000).toString();
}
return txs.sort(function (a, b) {
return txs.sort(function (a: { timestamp: number }, b: { timestamp: number }) {
return b.timestamp - a.timestamp;
});
}
async fetchPendingTransactions() {
if (!this._api) throw new Error('Internal error: _api is not initialized');
const response = await this._api.get('/getpending', {
headers: {
'Access-Control-Allow-Origin': '*',
@ -426,6 +443,7 @@ export class LightningCustodianWallet extends LegacyWallet {
}
async fetchTransactions() {
if (!this._api) throw new Error('Internal error: _api is not initialized');
// TODO: iterate over all available pages
const limit = 10;
let queryRes = '';
@ -462,7 +480,8 @@ export class LightningCustodianWallet extends LegacyWallet {
return this.balance;
}
async fetchBalance(noRetry) {
async fetchBalance(noRetry?: boolean): Promise<void> {
if (!this._api) throw new Error('Internal error: _api is not initialized');
await this.checkLogin();
const response = await this._api.get('/balance', {
@ -490,7 +509,6 @@ export class LightningCustodianWallet extends LegacyWallet {
throw new Error('API unexpected response: ' + JSON.stringify(response.body));
}
this.balance_raw = json;
this.balance = json.BTC.AvailableBalance;
this._lastBalanceFetch = +new Date();
}
@ -511,14 +529,14 @@ export class LightningCustodianWallet extends LegacyWallet {
* @param invoice BOLT invoice string
* @return {payment_hash: string}
*/
decodeInvoice(invoice) {
decodeInvoice(invoice: string) {
const { payeeNodeKey, tags, satoshis, millisatoshis, timestamp } = bolt11.decode(invoice);
const decoded = {
const decoded: any = {
destination: payeeNodeKey,
num_satoshis: satoshis ? satoshis.toString() : '0',
num_millisatoshis: millisatoshis ? millisatoshis.toString() : '0',
timestamp: timestamp.toString(),
timestamp: timestamp?.toString() ?? '0',
fallback_addr: '',
route_hints: [],
};
@ -554,6 +572,7 @@ export class LightningCustodianWallet extends LegacyWallet {
}
async fetchInfo() {
if (!this._api) throw new Error('Internal error: _api is not initialized');
const response = await this._api.get('/getinfo', {
headers: {
'Access-Control-Allow-Origin': '*',
@ -574,10 +593,9 @@ export class LightningCustodianWallet extends LegacyWallet {
if (!json.identity_pubkey) {
throw new Error('API unexpected response: ' + JSON.stringify(response.body));
}
this.info_raw = json;
}
static async isValidNodeAddress(address) {
static async isValidNodeAddress(address: string) {
const apiCall = new Frisbee({
baseURI: address,
});
@ -622,7 +640,8 @@ export class LightningCustodianWallet extends LegacyWallet {
* @param invoice BOLT invoice string
* @return {Promise.<Object>}
*/
async decodeInvoiceRemote(invoice) {
async decodeInvoiceRemote(invoice: string) {
if (!this._api) throw new Error('Internal error: _api is not initialized');
await this.checkLogin();
const response = await this._api.get('/decodeinvoice?invoice=' + invoice, {
@ -649,7 +668,7 @@ export class LightningCustodianWallet extends LegacyWallet {
return (this.decoded_invoice_raw = json);
}
weOwnTransaction(txid) {
weOwnTransaction(txid: string) {
for (const tx of this.getTransactions()) {
if (tx && tx.payment_hash && tx.payment_hash === txid) return true;
}
@ -657,7 +676,7 @@ export class LightningCustodianWallet extends LegacyWallet {
return false;
}
authenticate(lnurl) {
authenticate(lnurl: any) {
return lnurl.authenticate(this.secret);
}
}

View File

@ -1,19 +1,24 @@
import RNFS from 'react-native-fs';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import RnLdk from 'rn-ldk/src/index';
import { LightningCustodianWallet } from './lightning-custodian-wallet';
import SyncedAsyncStorage from '../synced-async-storage';
import { randomBytes } from '../rng';
import * as bip39 from 'bip39';
import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet';
import * as bitcoin from 'bitcoinjs-lib';
import bolt11 from 'bolt11';
import { SegwitBech32Wallet } from './segwit-bech32-wallet';
import RNFS from 'react-native-fs';
import RnLdk from 'rn-ldk/src/index';
import presentAlert from '../../components/Alert';
const bitcoin = require('bitcoinjs-lib');
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import { randomBytes } from '../rng';
import SyncedAsyncStorage from '../synced-async-storage';
import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet';
import { LightningCustodianWallet } from './lightning-custodian-wallet';
import { SegwitBech32Wallet } from './segwit-bech32-wallet';
export class LightningLdkWallet extends LightningCustodianWallet {
static type = 'lightningLdk';
static typeReadable = 'Lightning LDK';
static readonly type = 'lightningLdk';
static readonly typeReadable = 'Lightning LDK';
// @ts-ignore: override
public readonly type = LightningLdkWallet.type;
// @ts-ignore: override
public readonly typeReadable = LightningLdkWallet.typeReadable;
private _listChannels: any[] = [];
private _listPayments: any[] = [];
private _listInvoices: any[] = [];
@ -44,8 +49,8 @@ export class LightningLdkWallet extends LightningCustodianWallet {
return pubkeyHex;
}
constructor(props?: any) {
super(props);
constructor() {
super();
this.preferredBalanceUnit = BitcoinUnit.SATS;
this.chain = Chain.OFFCHAIN;
this.user_invoices_raw = []; // compatibility with other lightning wallet class
@ -202,8 +207,8 @@ export class LightningLdkWallet extends LightningCustodianWallet {
}
}
getAddress() {
return undefined;
getAddress(): string | false {
return false;
}
getSecret() {
@ -366,7 +371,7 @@ export class LightningLdkWallet extends LightningCustodianWallet {
}
}
async getUserInvoices(limit = false) {
async getUserInvoices(limit: number | false = false) {
const newInvoices: any[] = [];
let found = false;
@ -433,7 +438,7 @@ export class LightningLdkWallet extends LightningCustodianWallet {
return paymentRequest;
}
async getAddressAsync() {
async getAddressAsync(): Promise<string> {
throw new Error('getAddressAsync: Not implemented');
}

View File

@ -55,8 +55,12 @@ const electrumStandart = (passphrase?: string): SeedOpts => ({
const ELECTRUM_SEED_PREFIX = 'electrumseed:';
export class MultisigHDWallet extends AbstractHDElectrumWallet {
static type = 'HDmultisig';
static typeReadable = 'Multisig Vault';
static readonly type = 'HDmultisig';
static readonly typeReadable = 'Multisig Vault';
// @ts-ignore: override
public readonly type = MultisigHDWallet.type;
// @ts-ignore: override
public readonly typeReadable = MultisigHDWallet.typeReadable;
static FORMAT_P2WSH = 'p2wsh';
static FORMAT_P2SH_P2WSH = 'p2sh-p2wsh';

View File

@ -8,9 +8,13 @@ import { CoinSelectTarget } from 'coinselect';
const ECPair = ECPairFactory(ecc);
export class SegwitBech32Wallet extends LegacyWallet {
static type = 'segwitBech32';
static typeReadable = 'P2 WPKH';
static segwitType = 'p2wpkh';
static readonly type = 'segwitBech32';
static readonly typeReadable = 'P2 WPKH';
// @ts-ignore: override
public readonly type = SegwitBech32Wallet.type;
// @ts-ignore: override
public readonly typeReadable = SegwitBech32Wallet.typeReadable;
public readonly segwitType = 'p2wpkh';
getAddress(): string | false {
if (this._address) return this._address;

View File

@ -21,9 +21,13 @@ function pubkeyToP2shSegwitAddress(pubkey: Buffer): string | false {
}
export class SegwitP2SHWallet extends LegacyWallet {
static type = 'segwitP2SH';
static typeReadable = 'SegWit (P2SH)';
static segwitType = 'p2sh(p2wpkh)';
static readonly type = 'segwitP2SH';
static readonly typeReadable = 'SegWit (P2SH)';
// @ts-ignore: override
public readonly type = SegwitP2SHWallet.type;
// @ts-ignore: override
public readonly typeReadable = SegwitP2SHWallet.typeReadable;
public readonly segwitType = 'p2sh(p2wpkh)';
static witnessToAddress(witness: string): string | false {
try {

View File

@ -6,25 +6,32 @@ import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet';
import { HDSegwitP2SHWallet } from './hd-segwit-p2sh-wallet';
import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet';
type TWalletThis = Omit<HDLegacyP2PKHWallet | HDSegwitP2SHWallet | HDSegwitBech32Wallet, 'secret'> & {
secret: string[];
};
// collection of SLIP39 functions
const SLIP39Mixin = {
_getSeed() {
const master = slip39.recoverSecret(this.secret, this.passphrase);
const self = this as unknown as TWalletThis;
const master = slip39.recoverSecret(self.secret, self.passphrase);
return Buffer.from(master);
},
validateMnemonic() {
if (!this.secret.every(m => slip39.validateMnemonic(m))) return false;
const self = this as unknown as TWalletThis;
if (!self.secret.every(m => slip39.validateMnemonic(m))) return false;
try {
slip39.recoverSecret(this.secret);
slip39.recoverSecret(self.secret);
} catch (e) {
return false;
}
return true;
},
setSecret(newSecret) {
setSecret(newSecret: string) {
const self = this as unknown as TWalletThis;
// Try to match words to the default slip39 wordlist and complete partial words
const lookupMap = WORD_LIST.reduce((map, word) => {
const prefix3 = word.substr(0, 3);
@ -36,7 +43,7 @@ const SLIP39Mixin = {
return map;
}, new Map());
this.secret = newSecret
self.secret = newSecret
.trim()
.split('\n')
.filter(s => s)
@ -54,18 +61,23 @@ const SLIP39Mixin = {
return secret;
});
return this;
return self;
},
getID() {
const string2hash = this.secret.sort().join(',') + (this.getPassphrase() || '');
const self = this as unknown as TWalletThis;
const string2hash = self.secret.sort().join(',') + (self.getPassphrase() || '');
return createHash('sha256').update(string2hash).digest().toString('hex');
},
};
export class SLIP39LegacyP2PKHWallet extends HDLegacyP2PKHWallet {
static type = 'SLIP39legacyP2PKH';
static typeReadable = 'SLIP39 Legacy (P2PKH)';
static readonly type = 'SLIP39legacyP2PKH';
static readonly typeReadable = 'SLIP39 Legacy (P2PKH)';
// @ts-ignore: override
public readonly type = SLIP39LegacyP2PKHWallet.type;
// @ts-ignore: override
public readonly typeReadable = SLIP39LegacyP2PKHWallet.typeReadable;
allowBIP47() {
return false;
@ -73,23 +85,33 @@ export class SLIP39LegacyP2PKHWallet extends HDLegacyP2PKHWallet {
_getSeed = SLIP39Mixin._getSeed;
validateMnemonic = SLIP39Mixin.validateMnemonic;
// @ts-ignore: this type mismatch
setSecret = SLIP39Mixin.setSecret;
getID = SLIP39Mixin.getID;
}
export class SLIP39SegwitP2SHWallet extends HDSegwitP2SHWallet {
static type = 'SLIP39segwitP2SH';
static typeReadable = 'SLIP39 SegWit (P2SH)';
static readonly type = 'SLIP39segwitP2SH';
static readonly typeReadable = 'SLIP39 SegWit (P2SH)';
// @ts-ignore: override
public readonly type = SLIP39SegwitP2SHWallet.type;
// @ts-ignore: override
public readonly typeReadable = SLIP39SegwitP2SHWallet.typeReadable;
_getSeed = SLIP39Mixin._getSeed;
validateMnemonic = SLIP39Mixin.validateMnemonic;
// @ts-ignore: this type mismatch
setSecret = SLIP39Mixin.setSecret;
getID = SLIP39Mixin.getID;
}
export class SLIP39SegwitBech32Wallet extends HDSegwitBech32Wallet {
static type = 'SLIP39segwitBech32';
static typeReadable = 'SLIP39 SegWit (Bech32)';
static readonly type = 'SLIP39segwitBech32';
static readonly typeReadable = 'SLIP39 SegWit (Bech32)';
// @ts-ignore: override
public readonly type = SLIP39SegwitBech32Wallet.type;
// @ts-ignore: override
public readonly typeReadable = SLIP39SegwitBech32Wallet.typeReadable;
allowBIP47() {
return false;
@ -97,6 +119,7 @@ export class SLIP39SegwitBech32Wallet extends HDSegwitBech32Wallet {
_getSeed = SLIP39Mixin._getSeed;
validateMnemonic = SLIP39Mixin.validateMnemonic;
// @ts-ignore: this type mismatch
setSecret = SLIP39Mixin.setSecret;
getID = SLIP39Mixin.getID;
}

View File

@ -1,10 +1,14 @@
import * as bitcoin from 'bitcoinjs-lib';
import { SegwitBech32Wallet } from './segwit-bech32-wallet';
const bitcoin = require('bitcoinjs-lib');
export class TaprootWallet extends SegwitBech32Wallet {
static type = 'taproot';
static typeReadable = 'P2 TR';
static segwitType = 'p2wpkh';
static readonly type = 'taproot';
static readonly typeReadable = 'P2 TR';
// @ts-ignore: override
public readonly type = TaprootWallet.type;
// @ts-ignore: override
public readonly typeReadable = TaprootWallet.typeReadable;
public readonly segwitType = 'p2wpkh';
/**
* Converts script pub key to a Taproot address if it can. Returns FALSE if it cant.
@ -12,7 +16,7 @@ export class TaprootWallet extends SegwitBech32Wallet {
* @param scriptPubKey
* @returns {boolean|string} Either bech32 address or false
*/
static scriptPubKeyToAddress(scriptPubKey: string) {
static scriptPubKeyToAddress(scriptPubKey: string): string | false {
try {
const publicKey = Buffer.from(scriptPubKey, 'hex');
return bitcoin.address.fromOutputScript(publicKey, bitcoin.networks.bitcoin);

View File

@ -114,3 +114,5 @@ export type TWallet =
| SegwitBech32Wallet
| SegwitP2SHWallet
| WatchOnlyWallet;
export type THDWalletForWatchOnly = HDSegwitBech32Wallet | HDSegwitP2SHWallet | HDLegacyP2PKHWallet;

View File

@ -1,22 +1,26 @@
import { LegacyWallet } from './legacy-wallet';
import { HDSegwitP2SHWallet } from './hd-segwit-p2sh-wallet';
import BIP32Factory from 'bip32';
import * as bitcoin from 'bitcoinjs-lib';
import ecc from '../../blue_modules/noble_ecc';
import { AbstractWallet } from './abstract-wallet';
import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet';
import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet';
import BIP32Factory from 'bip32';
import ecc from '../../blue_modules/noble_ecc';
import { HDSegwitP2SHWallet } from './hd-segwit-p2sh-wallet';
import { LegacyWallet } from './legacy-wallet';
import { THDWalletForWatchOnly } from './types';
const bitcoin = require('bitcoinjs-lib');
const bip32 = BIP32Factory(ecc);
export class WatchOnlyWallet extends LegacyWallet {
static type = 'watchOnly';
static typeReadable = 'Watch-only';
static readonly type = 'watchOnly';
static readonly typeReadable = 'Watch-only';
// @ts-ignore: override
public readonly type = WatchOnlyWallet.type;
// @ts-ignore: override
public readonly typeReadable = WatchOnlyWallet.typeReadable;
constructor() {
super();
this.use_with_hardware_wallet = false;
this.masterFingerprint = false;
}
public _hdWalletInstance?: THDWalletForWatchOnly;
use_with_hardware_wallet = false;
masterFingerprint: number = 0;
/**
* @inheritDoc
@ -37,7 +41,7 @@ export class WatchOnlyWallet extends LegacyWallet {
}
allowSend() {
return this.useWithHardwareWalletEnabled() && this.isHd() && this._hdWalletInstance.allowSend();
return this.useWithHardwareWalletEnabled() && this.isHd() && this._hdWalletInstance!.allowSend();
}
allowSignVerifyMessage() {
@ -65,11 +69,9 @@ export class WatchOnlyWallet extends LegacyWallet {
* this method creates appropriate HD wallet class, depending on whether we have xpub, ypub or zpub
* as a property of `this`, and in case such property exists - it recreates it and copies data from old one.
* this is needed after serialization/save/load/deserialization procedure.
*
* @return {WatchOnlyWallet} this
*/
init() {
let hdWalletInstance;
let hdWalletInstance: THDWalletForWatchOnly;
if (this.secret.startsWith('xpub')) hdWalletInstance = new HDLegacyP2PKHWallet();
else if (this.secret.startsWith('ypub')) hdWalletInstance = new HDSegwitP2SHWallet();
else if (this.secret.startsWith('zpub')) hdWalletInstance = new HDSegwitBech32Wallet();
@ -84,6 +86,7 @@ export class WatchOnlyWallet extends LegacyWallet {
if (this._hdWalletInstance) {
// now, porting all properties from old object to new one
for (const k of Object.keys(this._hdWalletInstance)) {
// @ts-ignore: JS magic here
hdWalletInstance[k] = this._hdWalletInstance[k];
}
@ -117,6 +120,7 @@ export class WatchOnlyWallet extends LegacyWallet {
async fetchBalance() {
if (this.secret.startsWith('xpub') || this.secret.startsWith('ypub') || this.secret.startsWith('zpub')) {
if (!this._hdWalletInstance) this.init();
if (!this._hdWalletInstance) throw new Error('Internal error: _hdWalletInstance is not initialized');
return this._hdWalletInstance.fetchBalance();
} else {
// return LegacyWallet.prototype.fetchBalance.call(this);
@ -127,6 +131,7 @@ export class WatchOnlyWallet extends LegacyWallet {
async fetchTransactions() {
if (this.secret.startsWith('xpub') || this.secret.startsWith('ypub') || this.secret.startsWith('zpub')) {
if (!this._hdWalletInstance) this.init();
if (!this._hdWalletInstance) throw new Error('Internal error: _hdWalletInstance is not initialized');
return this._hdWalletInstance.fetchTransactions();
} else {
// return LegacyWallet.prototype.fetchBalance.call(this);
@ -134,18 +139,18 @@ export class WatchOnlyWallet extends LegacyWallet {
}
}
async getAddressAsync() {
async getAddressAsync(): Promise<string> {
if (this.isAddressValid(this.secret)) return new Promise(resolve => resolve(this.secret));
if (this._hdWalletInstance) return this._hdWalletInstance.getAddressAsync();
throw new Error('Not initialized');
}
_getExternalAddressByIndex(index) {
_getExternalAddressByIndex(index: number) {
if (this._hdWalletInstance) return this._hdWalletInstance._getExternalAddressByIndex(index);
throw new Error('Not initialized');
}
_getInternalAddressByIndex(index) {
_getInternalAddressByIndex(index: number) {
if (this._hdWalletInstance) return this._hdWalletInstance._getInternalAddressByIndex(index);
throw new Error('Not initialized');
}
@ -170,29 +175,30 @@ export class WatchOnlyWallet extends LegacyWallet {
throw new Error('Not initialized');
}
getUtxo(...args) {
getUtxo(...args: Parameters<THDWalletForWatchOnly['getUtxo']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.getUtxo(...args);
throw new Error('Not initialized');
}
combinePsbt(base64one, base64two) {
if (this._hdWalletInstance) return this._hdWalletInstance.combinePsbt(base64one, base64two);
combinePsbt(...args: Parameters<THDWalletForWatchOnly['combinePsbt']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.combinePsbt(...args);
throw new Error('Not initialized');
}
broadcastTx(hex) {
if (this._hdWalletInstance) return this._hdWalletInstance.broadcastTx(hex);
broadcastTx(...args: Parameters<THDWalletForWatchOnly['broadcastTx']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.broadcastTx(...args);
throw new Error('Not initialized');
}
/**
* signature of this method is the same ad BIP84 createTransaction, BUT this method should be used to create
* unsinged PSBT to be used with HW wallet (or other external signer)
* @see HDSegwitBech32Wallet.createTransaction
*/
createTransaction(utxos, targets, feeRate, changeAddress, sequence) {
createTransaction(...args: Parameters<THDWalletForWatchOnly['createTransaction']>) {
const [utxos, targets, feeRate, changeAddress, sequence] = args;
if (this._hdWalletInstance && this.isHd()) {
return this._hdWalletInstance.createTransaction(utxos, targets, feeRate, changeAddress, sequence, true, this.getMasterFingerprint());
const masterFingerprint = this.getMasterFingerprint();
return this._hdWalletInstance.createTransaction(utxos, targets, feeRate, changeAddress, sequence, true, masterFingerprint);
} else {
throw new Error('Not a HD watch-only wallet, cant create PSBT (or just not initialized)');
}
@ -224,7 +230,7 @@ export class WatchOnlyWallet extends LegacyWallet {
return this.secret.startsWith('xpub') || this.secret.startsWith('ypub') || this.secret.startsWith('zpub');
}
weOwnAddress(address) {
weOwnAddress(address: string) {
if (this.isHd()) {
if (this._hdWalletInstance) return this._hdWalletInstance.weOwnAddress(address);
throw new Error('Not initialized');
@ -243,7 +249,7 @@ export class WatchOnlyWallet extends LegacyWallet {
return !!this.use_with_hardware_wallet;
}
setUseWithHardwareWalletEnabled(enabled) {
setUseWithHardwareWalletEnabled(enabled: boolean) {
this.use_with_hardware_wallet = !!enabled;
}
@ -262,7 +268,7 @@ export class WatchOnlyWallet extends LegacyWallet {
if (this.secret.startsWith('zpub')) {
xpub = this._zpubToXpub(this.secret);
} else if (this.secret.startsWith('ypub')) {
xpub = this.constructor._ypubToXpub(this.secret);
xpub = AbstractWallet._ypubToXpub(this.secret);
} else {
xpub = this.secret;
}
@ -275,32 +281,32 @@ export class WatchOnlyWallet extends LegacyWallet {
return false;
}
addressIsChange(...args) {
addressIsChange(...args: Parameters<THDWalletForWatchOnly['addressIsChange']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.addressIsChange(...args);
return super.addressIsChange(...args);
}
getUTXOMetadata(...args) {
getUTXOMetadata(...args: Parameters<THDWalletForWatchOnly['getUTXOMetadata']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.getUTXOMetadata(...args);
return super.getUTXOMetadata(...args);
}
setUTXOMetadata(...args) {
setUTXOMetadata(...args: Parameters<THDWalletForWatchOnly['setUTXOMetadata']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.setUTXOMetadata(...args);
return super.setUTXOMetadata(...args);
}
getDerivationPath(...args) {
getDerivationPath(...args: Parameters<THDWalletForWatchOnly['getDerivationPath']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.getDerivationPath(...args);
throw new Error("Not a HD watch-only wallet, can't use derivation path");
}
setDerivationPath(...args) {
setDerivationPath(...args: Parameters<THDWalletForWatchOnly['setDerivationPath']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.setDerivationPath(...args);
throw new Error("Not a HD watch-only wallet, can't use derivation path");
}
isSegwit() {
isSegwit(): boolean {
if (this._hdWalletInstance) return this._hdWalletInstance.isSegwit();
return super.isSegwit();
}

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef, useContext, useCallback, useMemo }
import { Image, Text, TouchableOpacity, View, I18nManager, StyleSheet } from 'react-native';
import Clipboard from '@react-native-clipboard/clipboard';
import LinearGradient from 'react-native-linear-gradient';
import { AbstractWallet, HDSegwitBech32Wallet, LightningCustodianWallet, LightningLdkWallet, MultisigHDWallet } from '../class';
import { HDSegwitBech32Wallet, LightningCustodianWallet, LightningLdkWallet, MultisigHDWallet } from '../class';
import { BitcoinUnit } from '../models/bitcoinUnits';
import WalletGradient from '../class/wallet-gradient';
import Biometric from '../class/biometrics';
@ -11,9 +11,10 @@ import { BlueStorageContext } from '../blue_modules/storage-context';
import ToolTipMenu from './TooltipMenu';
import { BluePrivateBalance } from '../BlueComponents';
import { FiatUnit } from '../models/fiatUnit';
import { TWallet } from '../class/wallets/types';
interface TransactionsNavigationHeaderProps {
wallet: AbstractWallet;
wallet: TWallet;
onWalletUnitChange?: (wallet: any) => void;
navigation: {
navigate: (route: string, params?: any) => void;
@ -71,7 +72,7 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
}
};
const updateWalletVisibility = (w: AbstractWallet, newHideBalance: boolean) => {
const updateWalletVisibility = (w: TWallet, newHideBalance: boolean) => {
w.hideBalance = newHideBalance;
return w;
};
@ -90,7 +91,7 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
saveToDisk();
};
const updateWalletWithNewUnit = (w: AbstractWallet, newPreferredUnit: BitcoinUnit) => {
const updateWalletWithNewUnit = (w: TWallet, newPreferredUnit: BitcoinUnit) => {
w.preferredBalanceUnit = newPreferredUnit;
return w;
};

View File

@ -20,7 +20,7 @@ import loc, { formatBalance, transactionTimeToReadable } from '../loc';
import { LightningCustodianWallet, LightningLdkWallet, MultisigHDWallet } from '../class';
import WalletGradient from '../class/wallet-gradient';
import { BluePrivateBalance } from '../BlueComponents';
import { BlueStorageContext } from '../blue_modules/storage-context';
import { BlueStorageContext, WalletTransactionsStatus } from '../blue_modules/storage-context';
import { isTablet, isDesktop } from '../blue_modules/environment';
import { useTheme } from './themes';
@ -180,7 +180,7 @@ export const WalletCarouselItem = ({ item, _, onPress, handleLongPress, isSelect
}
const latestTransactionText =
walletTransactionUpdateStatus === true || walletTransactionUpdateStatus === item.getID()
walletTransactionUpdateStatus === WalletTransactionsStatus.ALL || walletTransactionUpdateStatus === item.getID()
? loc.transactions.updating
: item.getBalance() !== 0 && item.getLatestTransactionTime() === 0
? loc.wallets.pull_to_refresh

View File

@ -11,7 +11,6 @@ import Share from 'react-native-share';
import { useTheme } from '../themes';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import { BlueStorageContext } from '../../blue_modules/storage-context';
import { AbstractWallet } from '../../class';
import Biometric from '../../class/biometrics';
import presentAlert from '../Alert';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
@ -91,7 +90,7 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad
};
const handleCopyPrivkeyPress = () => {
const wallet = wallets.find((w: AbstractWallet) => w.getID() === walletID);
const wallet = wallets.find(w => w.getID() === walletID);
if (!wallet) {
presentAlert({ message: 'Internal error: cant find wallet' });
return;

View File

@ -164,15 +164,18 @@ const RateExtractors = {
},
} as const;
type FiatUnit = {
[key: string]: {
endPointKey: string;
symbol: string;
locale: string;
source: 'CoinDesk' | 'Yadio' | 'Exir' | 'wazirx' | 'Bitstamp';
};
export type TFiatUnit = {
endPointKey: string;
symbol: string;
locale: string;
source: 'CoinDesk' | 'Yadio' | 'Exir' | 'wazirx' | 'Bitstamp';
};
export const FiatUnit = untypedFiatUnit as FiatUnit;
export type TFiatUnits = {
[key: string]: TFiatUnit;
};
export const FiatUnit = untypedFiatUnit as TFiatUnits;
export type FiatUnitType = {
endPointKey: string;

View File

@ -34,7 +34,7 @@ const LdkInfo = () => {
const { wallets } = useContext(BlueStorageContext);
const refreshDataInterval = useRef<NodeJS.Timer>();
const sectionList = useRef<SectionList | null>();
const wallet: LightningLdkWallet = wallets.find((w: AbstractWallet) => w.getID() === walletID);
const wallet = wallets.find(w => w.getID() === walletID) as LightningLdkWallet;
const { colors } = useTheme();
const { setOptions, navigate } = useNavigation();
const name = useRoute().name;
@ -159,15 +159,9 @@ const LdkInfo = () => {
if (!(await confirm())) return;
setSelectedChannelIndex(undefined);
const wallets2use = wallets.filter((w: AbstractWallet) => w.chain === Chain.ONCHAIN);
const wallets2use = wallets.filter(w => w.chain === Chain.ONCHAIN);
const toWallet: AbstractWallet = await selectWallet(
navigate,
name,
null,
wallets2use,
'Onchain wallet is required to withdraw funds to',
);
const toWallet = await selectWallet(navigate, name, null, wallets2use, 'Onchain wallet is required to withdraw funds to');
// using wallets2use instead of simple Chain.ONCHAIN argument because by default this argument only selects wallets
// that can send, which is not possible if user wants to withdraw to watch-only wallet
if (!toWallet) return;
@ -190,14 +184,8 @@ const LdkInfo = () => {
};
const claimBalance = async () => {
const wallets2use = wallets.filter((w: AbstractWallet) => w.chain === Chain.ONCHAIN);
const selectedWallet: AbstractWallet = await selectWallet(
navigate,
name,
null,
wallets2use,
'Onchain wallet is required to withdraw funds to',
);
const wallets2use = wallets.filter(w => w.chain === Chain.ONCHAIN);
const selectedWallet = await selectWallet(navigate, name, null, wallets2use, 'Onchain wallet is required to withdraw funds to');
// using wallets2use instead of simple Chain.ONCHAIN argument because by default this argument only selects wallets
// that can send, which is not possible if user wants to withdraw to watch-only wallet
if (!selectedWallet) return;
@ -316,7 +304,7 @@ const LdkInfo = () => {
};
const navigateToOpenChannel = async ({ isPrivateChannel }: { isPrivateChannel: boolean }) => {
const availableWallets = [...wallets.filter((item: AbstractWallet) => item.isSegwit() && item.allowSend())];
const availableWallets = [...wallets.filter(item => item.isSegwit() && item.allowSend())];
if (availableWallets.length === 0) {
return presentAlert({ message: loc.lnd.refill_create });
}

View File

@ -9,7 +9,7 @@ import AddressInput from '../../components/AddressInput';
import AmountInput from '../../components/AmountInput';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import loc from '../../loc';
import { AbstractWallet, HDSegwitBech32Wallet, LightningLdkWallet } from '../../class';
import { HDSegwitBech32Wallet, LightningLdkWallet } from '../../class';
import { ArrowPicker } from '../../components/ArrowPicker';
import { Psbt } from 'bitcoinjs-lib';
import Biometric from '../../class/biometrics';
@ -45,8 +45,8 @@ const LdkOpenChannel = (props: any) => {
psbt,
remoteHostWithPubkey = '030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f@52.50.244.44:9735' /* Bitrefill */,
} = useRoute<LdkOpenChannelProps>().params;
const fundingWallet: HDSegwitBech32Wallet = wallets.find((w: AbstractWallet) => w.getID() === fundingWalletID);
const ldkWallet: LightningLdkWallet = wallets.find((w: AbstractWallet) => w.getID() === ldkWalletID);
const fundingWallet = wallets.find(w => w.getID() === fundingWalletID) as HDSegwitBech32Wallet;
const ldkWallet = wallets.find(w => w.getID() === ldkWalletID) as LightningLdkWallet;
const [unit, setUnit] = useState<BitcoinUnit | string>(ldkWallet.getPreferredBalanceUnit());
const [isLoading, setIsLoading] = useState(false);
const psbtOpenChannelStartedTs = useRef<number>();

View File

@ -8,7 +8,7 @@ import loc from '../../loc';
import { BlueStorageContext } from '../../blue_modules/storage-context';
import { useTheme } from '../../components/themes';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { AbstractWallet } from '../../class';
import { TWallet } from '../../class/wallets/types';
enum WalletActionType {
SetWallets = 'SET_WALLETS',
@ -18,7 +18,7 @@ enum WalletActionType {
}
interface WalletState {
wallets: AbstractWallet[];
wallets: TWallet[];
selectedWalletID: string | null;
isFocused: boolean;
}
@ -46,7 +46,7 @@ interface SetFocusAction {
interface SetWalletsAction {
type: WalletActionType.SetWallets;
wallets: AbstractWallet[];
wallets: TWallet[];
}
interface SelectWalletAction {
@ -89,7 +89,7 @@ const DrawerList: React.FC<DrawerListProps> = memo(({ navigation }) => {
};
const [state, dispatch] = useReducer(walletReducer, initialState);
const walletsCarousel = useRef<FlatList<AbstractWallet>>(null);
const walletsCarousel = useRef<FlatList<TWallet>>(null);
const { wallets } = useContext(BlueStorageContext);
const { colors } = useTheme();
const isFocused = useIsFocused();
@ -108,7 +108,7 @@ const DrawerList: React.FC<DrawerListProps> = memo(({ navigation }) => {
}, [wallets, isFocused]);
const handleClick = useCallback(
(item: AbstractWallet) => {
(item: TWallet) => {
if (item?.getID) {
const walletID = item.getID();
const walletType = item.type;

View File

@ -13,6 +13,7 @@ import {
View,
useColorScheme,
} from 'react-native';
import {
BitcoinButton,
BlueButtonLink,
@ -25,14 +26,7 @@ import {
} from '../../BlueComponents';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { BlueStorageContext } from '../../blue_modules/storage-context';
import {
AbstractWallet,
HDSegwitBech32Wallet,
HDSegwitP2SHWallet,
LightningCustodianWallet,
LightningLdkWallet,
SegwitP2SHWallet,
} from '../../class';
import { HDSegwitBech32Wallet, HDSegwitP2SHWallet, LightningCustodianWallet, LightningLdkWallet, SegwitP2SHWallet } from '../../class';
import presentAlert from '../../components/Alert';
import Button from '../../components/Button';
import { LdkButton } from '../../components/LdkButton';
@ -41,8 +35,7 @@ import { useTheme } from '../../components/themes';
import useAsyncPromise from '../../hooks/useAsyncPromise';
import loc from '../../loc';
import { Chain } from '../../models/bitcoinUnits';
const BlueApp = require('../../BlueApp');
const AppStorage = BlueApp.AppStorage;
import { AppStorage } from '../../BlueApp';
const A = require('../../blue_modules/analytics');
enum ButtonSelected {
@ -279,7 +272,7 @@ const WalletsAdd: React.FC = () => {
};
const createLightningLdkWallet = async () => {
const foundLdk = wallets.find((w: AbstractWallet) => w.type === LightningLdkWallet.type);
const foundLdk = wallets.find(w => w.type === LightningLdkWallet.type);
if (foundLdk) {
return presentAlert({ message: 'LDK wallet already exists' });
}

View File

@ -4,7 +4,6 @@ import { SectionList, StyleSheet, Text, View } from 'react-native';
import { BlueCopyTextToClipboard } from '../../BlueComponents';
import { PaymentCodeStackParamList } from '../../Navigation';
import { BlueStorageContext } from '../../blue_modules/storage-context';
import { AbstractHDElectrumWallet } from '../../class/wallets/abstract-hd-electrum-wallet';
import loc from '../../loc';
interface DataSection {
@ -22,12 +21,13 @@ export default function PaymentCodesList({ route }: Props) {
useEffect(() => {
if (!walletID) return;
const foundWallet: AbstractHDElectrumWallet = wallets.find((w: AbstractHDElectrumWallet) => w.getID() === walletID);
const foundWallet = wallets.find(w => w.getID() === walletID);
if (!foundWallet) return;
const newData: DataSection[] = [
{
title: loc.bip47.who_can_pay_me,
// @ts-ignore remove later
data: foundWallet.getBIP47SenderPaymentCodes(),
},
];

View File

@ -3,7 +3,6 @@ import React, { useCallback, useContext, useEffect, useState } from 'react';
import { ActivityIndicator, BackHandler, I18nManager, ScrollView, StyleSheet, Text, View } from 'react-native';
import { BlueStorageContext } from '../../blue_modules/storage-context';
import { AbstractWallet } from '../../class';
import Button from '../../components/Button';
import SafeArea from '../../components/SafeArea';
import { useTheme } from '../../components/themes';
@ -14,7 +13,7 @@ const PleaseBackup: React.FC = () => {
const { wallets } = useContext(BlueStorageContext);
const [isLoading, setIsLoading] = useState(true);
const { walletID } = useRoute().params as { walletID: string };
const wallet = wallets.find((w: AbstractWallet) => w.getID() === walletID);
const wallet = wallets.find(w => w.getID() === walletID);
const navigation = useNavigation();
const { colors } = useTheme();
const { enableBlur, disableBlur } = usePrivacy();

View File

@ -18,6 +18,7 @@ import {
findNodeHandle,
} from 'react-native';
import { Badge, Icon } from 'react-native-elements';
import {
BlueButtonLink,
BlueFormMultiInput,
@ -32,7 +33,7 @@ import { ViewEditMultisigCosignersStackParamsList } from '../../Navigation';
import * as NavigationService from '../../NavigationService';
import { BlueStorageContext } from '../../blue_modules/storage-context';
import { encodeUR } from '../../blue_modules/ur';
import { AbstractWallet, HDSegwitBech32Wallet, MultisigCosigner, MultisigHDWallet } from '../../class';
import { HDSegwitBech32Wallet, MultisigCosigner, MultisigHDWallet } from '../../class';
import Biometric from '../../class/biometrics';
import presentAlert from '../../components/Alert';
import BottomModal from '../../components/BottomModal';
@ -63,7 +64,7 @@ const ViewEditMultisigCosigners = ({ route }: Props) => {
const { navigate, goBack, dispatch, addListener } = useNavigation();
const openScannerButtonRef = useRef();
const { walletId } = route.params;
const w = useRef(wallets.find((wallet: AbstractWallet) => wallet.getID() === walletId));
const w = useRef(wallets.find(wallet => wallet.getID() === walletId));
const tempWallet = useRef(new MultisigHDWallet());
const [wallet, setWallet] = useState<MultisigHDWallet>();
const [isLoading, setIsLoading] = useState(true);
@ -176,6 +177,9 @@ const ViewEditMultisigCosigners = ({ route }: Props) => {
};
const onSave = async () => {
if (!wallet) {
throw new Error('Wallet is undefined');
}
setIsLoading(true);
const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled();
@ -188,9 +192,9 @@ const ViewEditMultisigCosigners = ({ route }: Props) => {
}
// eslint-disable-next-line prefer-const
let newWallets = wallets.filter((newWallet: MultisigHDWallet) => {
let newWallets = wallets.filter(newWallet => {
return newWallet.getID() !== walletId;
});
}) as MultisigHDWallet[];
if (!isElectrumDisabled) {
await wallet?.fetchBalance();
}

View File

@ -4,7 +4,6 @@ import { ActivityIndicator, InteractionManager, View } from 'react-native';
import Share from 'react-native-share';
import { BlueCopyTextToClipboard, BlueSpacing20, BlueText } from '../../BlueComponents';
import { BlueStorageContext } from '../../blue_modules/storage-context';
import { AbstractWallet } from '../../class';
import Biometric from '../../class/biometrics';
import Button from '../../components/Button';
import QRCodeComponent from '../../components/QRCodeComponent';
@ -26,7 +25,7 @@ const WalletXpub: React.FC = () => {
const { wallets } = useContext(BlueStorageContext);
const route = useRoute<WalletXpubRouteProp>();
const { walletID, xpub } = route.params;
const wallet = wallets.find((w: AbstractWallet) => w.getID() === walletID);
const wallet = wallets.find(w => w.getID() === walletID);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [xPubText, setXPubText] = useState<string | undefined>(undefined);
const navigation = useNavigation<NavigationProp<RootStackParamList, 'WalletXpub'>>();
@ -53,7 +52,7 @@ const WalletXpub: React.FC = () => {
}
const walletXpub = wallet.getXpub();
if (xpub !== walletXpub) {
navigation.setParams({ xpub: walletXpub });
navigation.setParams({ xpub: walletXpub || undefined });
}
setIsLoading(false);

View File

@ -3,7 +3,7 @@ const path = require('path');
const mainLocFile = './loc/en.json';
const dirsToInterate = ['components', 'screen', 'blue_modules', 'class'];
const addFiles = ['BlueComponents.js', 'App.js', 'BlueApp.js', 'Navigation.tsx'];
const addFiles = ['BlueComponents.js', 'App.js', 'BlueApp.ts', 'Navigation.tsx'];
const allowedLocPrefixes = ['loc.lnurl_auth', 'loc.units'];
const allLocKeysHashmap = {}; // loc key -> used or not

View File

@ -461,6 +461,7 @@ describe('BlueWallet UI Tests - no wallets', () => {
await helperSwitchAdvancedMode();
await yo('WalletsList');
await element(by.id('WalletsList')).swipe('left', 'fast', 1); // in case emu screen is small and it doesnt fit
await sleep(200); // Wait until bounce animation finishes.
// going to Import Wallet screen and importing Vault
await element(by.id('CreateAWallet')).tap();
await yo('ActivateVaultButton');
@ -538,6 +539,7 @@ describe('BlueWallet UI Tests - no wallets', () => {
await device.launchApp({ newInstance: true });
await yo('WalletsList');
await element(by.id('WalletsList')).swipe('left', 'fast', 1); // in case emu screen is small and it doesnt fit
await sleep(200); // Wait until bounce animation finishes.
// going to Import Wallet screen and importing mnemonic
await element(by.id('CreateAWallet')).tap();
await element(by.id('ImportWallet')).tap();
@ -672,7 +674,7 @@ describe('BlueWallet UI Tests - no wallets', () => {
await helperSwitchAdvancedMode();
await element(by.id('WalletsList')).swipe('left', 'fast', 1); // in case emu screen is small and it doesnt fit
await sleep(100); // wait swipe animation to finish
await sleep(200); // Wait until bounce animation finishes.
// going to Import Wallet screen and importing mnemonic
await element(by.id('CreateAWallet')).tap();
await element(by.id('ScrollView')).swipe('up', 'fast', 0.9); // in case emu screen is small and it doesnt fit

View File

@ -16,6 +16,7 @@ export async function helperImportWallet(importText, walletType, expectedWalletL
await yo('WalletsList');
await element(by.id('WalletsList')).swipe('left', 'fast', 1); // in case emu screen is small and it doesnt fit
await sleep(200); // Wait until bounce animation finishes.
// going to Import Wallet screen and importing mnemonic
await element(by.id('CreateAWallet')).tap();
await element(by.id('ImportWallet')).tap();
@ -114,6 +115,7 @@ export const expectToBeVisible = async id => {
export async function helperCreateWallet(walletName) {
await element(by.id('WalletsList')).swipe('left', 'fast', 1); // in case emu screen is small and it doesnt fit
await sleep(200); // Wait until bounce animation finishes.
await element(by.id('CreateAWallet')).tap();
await element(by.id('WalletNameInput')).replaceText(walletName || 'cr34t3d');
await yo('ActivateBitcoinButton');

View File

@ -33,7 +33,7 @@ describe('Watch only wallet', () => {
w.setSecret(secret);
assert.ok(w.valid());
assert.strictEqual(w.isHd(), true);
assert.strictEqual(w.getMasterFingerprint(), false);
assert.strictEqual(w.getMasterFingerprint(), 0);
assert.strictEqual(w.getMasterFingerprintHex(), '00000000');
assert.ok(w.isXpubValid(), w.secret);
assert.ok(!w.useWithHardwareWalletEnabled());

View File

@ -1 +1,3 @@
declare function alert(message: string): void;
declare const navigator: undefined | { product: 'ReactNative' };

8
typings/slip39.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
declare module 'slip39' {
export function recoverSecret(secret: string[], passphrase?: string): Buffer;
export function validateMnemonic(mnemonic: string): boolean;
}
declare module 'slip39/dist/slip39_helper' {
export const WORD_LIST: string[];
}