Merge branch 'master' into lighningse

This commit is contained in:
Marcos Rodriguez Velez 2025-02-14 21:18:16 -04:00
commit f4125cb1e9
5 changed files with 204 additions and 109 deletions

View file

@ -138,31 +138,40 @@ async function _getRealm() {
}
export const getPreferredServer = async (): Promise<ElectrumServerItem | undefined> => {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
const host = (await DefaultPreference.get(ELECTRUM_HOST)) as string;
const tcpPort = await DefaultPreference.get(ELECTRUM_TCP_PORT);
const sslPort = await DefaultPreference.get(ELECTRUM_SSL_PORT);
try {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
const host = (await DefaultPreference.get(ELECTRUM_HOST)) as string;
const tcpPort = await DefaultPreference.get(ELECTRUM_TCP_PORT);
const sslPort = await DefaultPreference.get(ELECTRUM_SSL_PORT);
console.log('Getting preferred server:', { host, tcpPort, sslPort });
console.log('Getting preferred server:', { host, tcpPort, sslPort });
if (!host) {
console.warn('Preferred server host is undefined');
return;
if (!host) {
console.warn('Preferred server host is undefined');
return;
}
return {
host,
tcp: tcpPort ? Number(tcpPort) : undefined,
ssl: sslPort ? Number(sslPort) : undefined,
};
} catch (error) {
console.error('Error in getPreferredServer:', error);
return undefined;
}
return {
host,
tcp: tcpPort ? Number(tcpPort) : undefined,
ssl: sslPort ? Number(sslPort) : undefined,
};
};
export const removePreferredServer = async () => {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
console.log('Removing preferred server');
await DefaultPreference.clear(ELECTRUM_HOST);
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
try {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
console.log('Removing preferred server');
await DefaultPreference.clear(ELECTRUM_HOST);
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
} catch (error) {
console.error('Error in removePreferredServer:', error);
}
};
export async function isDisabled(): Promise<boolean> {
@ -204,26 +213,31 @@ function getNextPeer() {
}
async function getSavedPeer(): Promise<Peer | null> {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
const host = (await DefaultPreference.get(ELECTRUM_HOST)) as string;
const tcpPort = await DefaultPreference.get(ELECTRUM_TCP_PORT);
const sslPort = await DefaultPreference.get(ELECTRUM_SSL_PORT);
try {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
const host = (await DefaultPreference.get(ELECTRUM_HOST)) as string;
const tcpPort = await DefaultPreference.get(ELECTRUM_TCP_PORT);
const sslPort = await DefaultPreference.get(ELECTRUM_SSL_PORT);
console.log('Getting saved peer:', { host, tcpPort, sslPort });
console.log('Getting saved peer:', { host, tcpPort, sslPort });
if (!host) {
if (!host) {
return null;
}
if (sslPort) {
return { host, ssl: Number(sslPort) };
}
if (tcpPort) {
return { host, tcp: Number(tcpPort) };
}
return null;
} catch (error) {
console.error('Error in getSavedPeer:', error);
return null;
}
if (sslPort) {
return { host, ssl: Number(sslPort) };
}
if (tcpPort) {
return { host, tcp: Number(tcpPort) };
}
return null;
}
export async function connectMain(): Promise<void> {
@ -262,7 +276,8 @@ export async function connectMain(): Promise<void> {
// most likely got a timeout from electrum ping. lets reconnect
// but only if we were previously connected (mainConnected), otherwise theres other
// code which does connection retries
mainClient.close();
mainClient?.close();
mainClient = undefined;
mainConnected = false;
// dropping `mainConnected` flag ensures there wont be reconnection race condition if several
// errors triggered
@ -310,12 +325,15 @@ export async function connectMain(): Promise<void> {
} catch (e) {
mainConnected = false;
console.log('bad connection:', JSON.stringify(usingPeer), e);
mainClient?.close();
mainClient = undefined;
}
if (!mainConnected) {
console.log('retry');
connectionAttempt = connectionAttempt + 1;
mainClient.close && mainClient.close();
mainClient?.close();
mainClient = undefined;
if (connectionAttempt >= 5) {
presentNetworkErrorAlert(usingPeer);
} else {
@ -407,7 +425,8 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
text: loc.wallets.list_tryagain,
onPress: () => {
connectionAttempt = 0;
mainClient.close() && mainClient.close();
mainClient?.close();
mainClient = undefined;
setTimeout(connectMain, 500);
},
style: 'default',
@ -418,7 +437,8 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
presentResetToDefaultsAlert().then(result => {
if (result) {
connectionAttempt = 0;
mainClient.close() && mainClient.close();
mainClient?.close();
mainClient = undefined;
setTimeout(connectMain, 500);
}
});
@ -429,7 +449,8 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
text: loc._.cancel,
onPress: () => {
connectionAttempt = 0;
mainClient.close() && mainClient.close();
mainClient?.close();
mainClient = undefined;
},
style: 'cancel',
},
@ -474,13 +495,18 @@ async function getRandomDynamicPeer(): Promise<Peer> {
}
export const getBalanceByAddress = async function (address: string): Promise<{ confirmed: number; unconfirmed: number }> {
if (!mainClient) throw new Error('Electrum client is not connected');
const script = bitcoin.address.toOutputScript(address);
const hash = bitcoin.crypto.sha256(script);
const reversedHash = Buffer.from(hash).reverse();
const balance = await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex'));
balance.addr = address;
return balance;
try {
if (!mainClient) throw new Error('Electrum client is not connected');
const script = bitcoin.address.toOutputScript(address);
const hash = bitcoin.crypto.sha256(script);
const reversedHash = Buffer.from(hash).reverse();
const balance = await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex'));
balance.addr = address;
return balance;
} catch (error) {
console.error('Error in getBalanceByAddress:', error);
throw error;
}
};
export const getConfig = async function () {
@ -958,25 +984,29 @@ export async function multiGetTransactionByTxid<T extends boolean>(
}
// saving cache:
realm.write(() => {
for (const txid of Object.keys(ret)) {
const tx = ret[txid];
// dont cache immature txs, but only for 'verbose', since its fully decoded tx jsons. non-verbose are just plain
// strings txhex
if (verbose && typeof tx !== 'string' && (!tx?.confirmations || tx.confirmations < 7)) {
continue;
}
try {
realm.write(() => {
for (const txid of Object.keys(ret)) {
const tx = ret[txid];
// dont cache immature txs, but only for 'verbose', since its fully decoded tx jsons. non-verbose are just plain
// strings txhex
if (verbose && typeof tx !== 'string' && (!tx?.confirmations || tx.confirmations < 7)) {
continue;
}
realm.create(
'Cache',
{
cache_key: txid + cacheKeySuffix,
cache_value: JSON.stringify(ret[txid]),
},
Realm.UpdateMode.Modified,
);
}
});
realm.create(
'Cache',
{
cache_key: txid + cacheKeySuffix,
cache_value: JSON.stringify(ret[txid]),
},
Realm.UpdateMode.Modified,
);
}
});
} catch (writeError) {
console.error('Failed to write transaction cache:', writeError);
}
return ret;
}

View file

@ -1,14 +1,15 @@
import React, { createContext, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { InteractionManager } from 'react-native';
import { InteractionManager, LayoutAnimation } from 'react-native';
import A from '../../blue_modules/analytics';
import { BlueApp as BlueAppClass, LegacyWallet, TCounterpartyMetadata, TTXMetadata, WatchOnlyWallet } from '../../class';
import type { TWallet } from '../../class/wallets/types';
import presentAlert from '../../components/Alert';
import loc from '../../loc';
import loc, { formatBalanceWithoutSuffix } from '../../loc';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { startAndDecrypt } from '../../blue_modules/start-and-decrypt';
import { isNotificationsEnabled, majorTomToGroundControl, unsubscribe } from '../../blue_modules/notifications';
import { BitcoinUnit } from '../../models/bitcoinUnits';
const BlueApp = BlueAppClass.getInstance();
@ -50,6 +51,7 @@ interface StorageContextType {
getItem: typeof BlueApp.getItem;
setItem: typeof BlueApp.setItem;
handleWalletDeletion: (walletID: string, forceDelete?: boolean) => Promise<boolean>;
confirmWalletDeletion: (wallet: any, onConfirmed: () => void) => void;
}
export enum WalletTransactionsStatus {
@ -111,7 +113,7 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
if (forceDelete) {
deleteWallet(wallet);
saveToDisk(true);
await saveToDisk(true);
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
return true;
}
@ -121,28 +123,35 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
isNotificationsSettingsEnabled = await isNotificationsEnabled();
} catch (error) {
console.error(`handleWalletDeletion: error checking notifications for wallet ${walletID}`, error);
presentAlert({
title: loc.errors.error,
message: loc.wallets.details_delete_wallet_error_message,
buttons: [
{
text: loc.wallets.details_delete_anyway,
onPress: async () => await handleWalletDeletion(walletID, true),
style: 'destructive',
},
{
text: loc.wallets.list_tryagain,
onPress: async () => await handleWalletDeletion(walletID),
},
{
text: loc._.cancel,
onPress: () => {},
style: 'cancel',
},
],
options: { cancelable: false },
return await new Promise<boolean>(resolve => {
presentAlert({
title: loc.errors.error,
message: loc.wallets.details_delete_wallet_error_message,
buttons: [
{
text: loc.wallets.details_delete_anyway,
onPress: async () => {
const result = await handleWalletDeletion(walletID, true);
resolve(result);
},
style: 'destructive',
},
{
text: loc.wallets.list_tryagain,
onPress: async () => {
const result = await handleWalletDeletion(walletID);
resolve(result);
},
},
{
text: loc._.cancel,
onPress: () => resolve(false),
style: 'cancel',
},
],
options: { cancelable: false },
});
});
return false;
}
try {
@ -167,41 +176,41 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
}
deleteWallet(wallet);
console.debug(`handleWalletDeletion: wallet ${walletID} deleted successfully`);
saveToDisk(true);
await saveToDisk(true);
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
return true;
} catch (e: unknown) {
console.error(`handleWalletDeletion: encountered error for wallet ${walletID}`, e);
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
if (forceDelete) {
deleteWallet(wallet);
saveToDisk(true);
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
return true;
} else {
return await new Promise<boolean>(resolve => {
presentAlert({
title: loc.errors.error,
message: loc.wallets.details_delete_wallet_error_message,
buttons: [
{
text: loc.wallets.details_delete_anyway,
onPress: async () => await handleWalletDeletion(walletID, true),
onPress: async () => {
const result = await handleWalletDeletion(walletID, true);
resolve(result);
},
style: 'destructive',
},
{
text: loc.wallets.list_tryagain,
onPress: async () => await handleWalletDeletion(walletID),
onPress: async () => {
const result = await handleWalletDeletion(walletID);
resolve(result);
},
},
{
text: loc._.cancel,
onPress: () => {},
onPress: () => resolve(false),
style: 'cancel',
},
],
options: { cancelable: false },
});
return false;
}
});
}
},
[deleteWallet, saveToDisk, wallets],
@ -364,6 +373,36 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
[wallets, addWallet, saveToDisk],
);
function confirmWalletDeletion(wallet: any, onConfirmed: () => void) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationWarning);
try {
const balance = formatBalanceWithoutSuffix(wallet.getBalance(), BitcoinUnit.SATS, true);
presentAlert({
title: loc.wallets.details_delete_wallet,
message: loc.formatString(loc.wallets.details_del_wb_q, { balance }),
buttons: [
{
text: loc.wallets.details_delete,
onPress: () => {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
onConfirmed();
},
style: 'destructive',
},
{
text: loc._.cancel,
onPress: () => {},
style: 'cancel',
},
],
options: { cancelable: false },
});
} catch (error) {
// Handle error silently if needed
}
}
const value: StorageContextType = useMemo(
() => ({
wallets,
@ -400,6 +439,7 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
walletTransactionUpdateStatus,
setWalletTransactionUpdateStatus,
handleWalletDeletion,
confirmWalletDeletion,
}),
[
wallets,

View file

@ -14,6 +14,7 @@
"storage_is_encrypted": "Twoje dane są zaszyfrowane. Hasło jest wymagane do ich rozszyfrowania",
"yes": "Tak",
"no": "Nie",
"save": "Zapisz...",
"seed": "Seed",
"success": "Sukces",
"wallet_key": "Klucz Portfela",
@ -106,7 +107,8 @@
"maxSatsFull": "Maksymalna kwota to {max} satoshi lub {currency}",
"minSats": "Kwota minimalna to {min} satoshi",
"minSatsFull": "Kwota minimalna to {min} satoshi lub {currency}",
"qrcode_for_the_address": "Kod QR dla adresu"
"qrcode_for_the_address": "Kod QR dla adresu",
"bip47_explanation": "Kody płatności to uniwersalne adresy, które chronią prywatność Twojego portfela. Nie wszystkie usługi je obsługują."
},
"send": {
"provided_address_is_invoice": "Ten adres wygląda na fakturę Lightning. Przejdź do swojego portfela Lightning aby ją opłacić.",
@ -252,7 +254,9 @@
"electrum_preferred_server_description": "Wprowadź serwer, którego ma używać twój portfel do wszystkich operacji związanych z Bitcoinem. Po zapisaniu ustawień, portfel będzie korzystał wyłącznie z tego serwera do sprawdzania sald, wysyłania transakcji oraz pobierania danych z sieci. Upewnij się, że masz zaufanie do tego serwera przed jego wyborem.",
"electrum_unable_to_connect": "Nie można się połączyć z {server}.",
"electrum_history": "Historia",
"electrum_reset_to_default": "To pozwoli BlueWallet losowo wybrać serwer z listy.",
"electrum_reset": "Ustaw wartości domyślne",
"electrum_reset_to_default_and_clear_history": "Przywróć domyślne ustawienia i wyczyść historię",
"encrypt_decrypt": "Odszyfruj Magazyn Danych",
"encrypt_decrypt_q": "Czy jesteś pewien, że chcesz odszyfrować schowek? To pozwoli na dostęp do twoich portfeli bez hasła.",
"encrypt_enc_and_pass": "Szyfrowany i chroniony hasłem",
@ -488,7 +492,9 @@
"identity_pubkey": "Klucz publiczny tożsamości",
"xpub_title": "XPUB portfela",
"manage_wallets_search_placeholder": "Szukaj portfeli, notatek",
"more_info": "Więcej informacji"
"more_info": "Więcej informacji",
"details_delete_wallet_error_message": "Nie udało się potwierdzić usunięcia tego portfela z powiadomień możliwe, że przyczyną jest problem z siecią lub słabe połączenie. Jeśli kontynuujesz, możesz nadal otrzymywać powiadomienia o transakcjach związanych z tym portfelem, nawet po jego usunięciu.",
"details_delete_anyway": "Usuń mimo to"
},
"total_balance_view": {
"display_in_bitcoin": "Wyświetlaj w Bitcoinie",
@ -503,6 +509,10 @@
"default_label": "Skarbiec wielopodpisowy",
"multisig_vault_explain": "Najlepsze bezpieczeństwo dla dużych kwot",
"provide_signature": "Podaj podpis",
"provide_signature_details": "Użyj urządzenia i portfela, w którym znajduje się klucz, aby podpisać tę transakcję.",
"provide_signature_details_bluewallet": "W BlueWallet przejdź do menu ekranu wysyłania i wybierz ",
"provide_signature_next_steps": "Skanuj lub importuj podpisaną transakcję",
"provide_signature_next_steps_details": "Gdy Twój portfel pomyślnie podpisze transakcję, zeskanuj podany kod QR lub zaimportuj dołączony plik, a następnie zweryfikuj wszystkie szczegóły przed wysłaniem jej.",
"vault_key": "Klucz Skarbca {number}",
"required_keys_out_of_total": "Wymagane klucze spośród wszystkich",
"fee": "Opłata: {number}",
@ -664,7 +674,7 @@
"notification_tx_unconfirmed": "Transakcja powiadomienia nie została jeszcze potwierdzona, proszę czekać",
"failed_create_notif_tx": "Nie udało się utworzyć transakcji on-chain",
"onchain_tx_needed": "Wymagana transakcja on-chain",
"notif_tx_sent": "Transakcja powiadomienia wysłana. Proszę czekać na jej potwierdzenie",
"notif_tx_sent" : "Transakcja powiadomienia wysłana. Proszę czekać na jej potwierdzenie",
"notif_tx": "Transakcja powiadomienia",
"not_found": "Kod płatności nie znaleziony"
}

View file

@ -173,6 +173,8 @@ const ElectrumSettings: React.FC = () => {
const serverSslPort = v?.ssl ? v.ssl.toString() : sslPort?.toString() || '';
if (serverHost && (serverPort || serverSslPort)) {
const testConnect = await BlueElectrum.testConnection(serverHost, Number(serverPort), Number(serverSslPort));
if (!testConnect) return;
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
// Clear current data for the preferred host
@ -197,6 +199,8 @@ const ElectrumSettings: React.FC = () => {
await DefaultPreference.set(BlueElectrum.ELECTRUM_SERVER_HISTORY, JSON.stringify(Array.from(newServerHistory)));
setServerHistory(newServerHistory);
}
} else {
throw new Error(loc.settings.electrum_error_connect);
}
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);

View file

@ -89,8 +89,12 @@ const WalletDetails: React.FC = () => {
const navigateToOverviewAndDeleteWallet = useCallback(async () => {
setIsLoading(true);
await handleWalletDeletion(wallet.getID());
popToTop();
const deletionSucceeded = await handleWalletDeletion(wallet.getID());
if (deletionSucceeded) {
popToTop();
} else {
setIsLoading(false);
}
}, [handleWalletDeletion, wallet]);
const presentWalletHasBalanceAlert = useCallback(async () => {
@ -129,10 +133,10 @@ const WalletDetails: React.FC = () => {
text: loc.wallets.details_yes_delete,
onPress: async () => {
const isBiometricsEnabled = await isBiometricUseCapableAndEnabled();
if (isBiometricsEnabled) {
if (!(await unlockWithBiometrics())) {
return;
setIsLoading(false);
return false;
}
}
if (wallet.getBalance && wallet.getBalance() > 0 && wallet.allowSend && wallet.allowSend()) {
@ -143,7 +147,14 @@ const WalletDetails: React.FC = () => {
},
style: 'destructive',
},
{ text: loc._.cancel, onPress: () => {}, style: 'cancel' },
{
text: loc._.cancel,
onPress: () => {
setIsLoading(false);
return false;
},
style: 'cancel',
},
],
options: { cancelable: false },
});