fix: wake up (#8537)
Some checks are pending
Build Release and Upload to TestFlight (iOS) / build (push) Waiting to run
Build Release and Upload to TestFlight (iOS) / testflight-upload (push) Blocked by required conditions
BuildReleaseApk / buildReleaseApk (push) Waiting to run
BuildReleaseApk / browserstack (push) Blocked by required conditions

This commit is contained in:
Nuno 2026-05-13 16:34:14 +02:00 committed by GitHub
parent d09bd69b96
commit 8195855f05
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 98 additions and 20 deletions

View File

@ -560,6 +560,7 @@ export const getMempoolTransactionsByAddress = async function (address: string):
export const ping = async function () {
try {
if (!mainClient) return false;
await mainClient.server_ping();
return true;
} catch (_) {}
@ -568,6 +569,25 @@ export const ping = async function () {
return false;
};
/**
* Verifies Electrum with server_ping. If the TCP socket died while the app was backgrounded,
* JS may still think we are connected ping fails, so we tear down the client and reconnect.
*/
export async function ensureElectrumConnection(): Promise<boolean> {
if (await isDisabled()) return true;
const believedConnected = mainConnected;
if (await ping()) return true;
console.log('ensureElectrumConnection: ping failed, forcing reconnect');
mainClient?.close();
mainClient = undefined;
mainConnected = false;
if (believedConnected) {
connectionAttempt = 0;
}
await connectMain();
return ping();
}
// exported only to be used in unit tests
export function txhexToElectrumTransaction(txhex: string): ElectrumTransactionWithHex {
const tx = bitcoin.Transaction.fromHex(txhex);
@ -1242,6 +1262,8 @@ export const testConnection = async function (host: string, tcpPort?: number, ss
export const forceDisconnect = (): void => {
mainClient?.close();
mainClient = undefined;
mainConnected = false;
};
export const setBatchingDisabled = () => {

View File

@ -1,5 +1,5 @@
import React, { createContext, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { LayoutAnimation } from 'react-native';
import { AppState, AppStateStatus, LayoutAnimation } from 'react-native';
import { BlueApp as BlueAppClass, TCounterpartyMetadata, TTXMetadata } from '../../class/blue-app';
import { LegacyWallet } from '../../class/wallets/legacy-wallet';
import { WatchOnlyWallet } from '../../class/wallets/watch-only-wallet';
@ -331,15 +331,10 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
console.debug('[refreshAllWalletTransactions] Setting wallet transaction status to ALL');
setWalletTransactionUpdateStatus(WalletTransactionsStatus.ALL);
}
console.debug('[refreshAllWalletTransactions] Ensuring Electrum connection (ping / reconnect if stale)...');
await BlueElectrum.ensureElectrumConnection();
console.debug('[refreshAllWalletTransactions] Waiting for connectivity...');
await BlueElectrum.waitTillConnected();
if (!(await BlueElectrum.ping())) {
// above `waitTillConnected` is not reliable, as app might have returned from long sleep, so it thinks its
// connected but actually socket is closed. thus, we ping, and if it fails - we wait again (reconnection code
// should pick up)
console.log('[refreshAllWalletTransactions] ping failed, waiting for connection...');
await BlueElectrum.waitTillConnected();
}
console.debug('[refreshAllWalletTransactions] Connected to Electrum');
@ -408,6 +403,52 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
[saveToDisk],
);
const refreshAllWalletTransactionsRef = useRef(refreshAllWalletTransactions);
refreshAllWalletTransactionsRef.current = refreshAllWalletTransactions;
useEffect(() => {
if (!walletsInitialized) return;
let wasInBackground = false;
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
const DEBOUNCE_MS = 1200;
const onAppStateChange = (next: AppStateStatus) => {
if (next === 'background') {
wasInBackground = true;
}
if (next !== 'active') {
if (debounceTimer) {
clearTimeout(debounceTimer);
debounceTimer = null;
}
return;
}
if (!wasInBackground) return;
wasInBackground = false;
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
debounceTimer = null;
(async () => {
if (AppState.currentState !== 'active') return;
if (await BlueElectrum.isDisabled()) return;
await refreshAllWalletTransactionsRef.current(undefined, false);
})().catch(() => {
/* refresh logs errors internally */
});
}, DEBOUNCE_MS);
};
const subscription = AppState.addEventListener('change', onAppStateChange);
return () => {
subscription.remove();
if (debounceTimer) clearTimeout(debounceTimer);
};
}, [walletsInitialized]);
const fetchAndSaveWalletTransactions = useCallback(
async (walletID: string) => {
const index = wallets.findIndex(wallet => wallet.getID() === walletID);
@ -419,6 +460,7 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
}
_lastTimeTriedToRefetchWallet[walletID] = Date.now();
await BlueElectrum.ensureElectrumConnection();
await BlueElectrum.waitTillConnected();
setWalletTransactionUpdateStatus(walletID);

View File

@ -116,13 +116,16 @@ const DetailViewStackScreensStack = () => {
if (isElectrumDisabled) return;
const subscription = AppState.addEventListener('change', nextState => {
if (nextState === 'active') {
pollConnection();
BlueElectrum.ensureElectrumConnection()
.then(ok => setElectrumConnected(ok))
.catch(() => setElectrumConnected(false));
}
});
return () => subscription.remove();
}, [isElectrumDisabled, pollConnection]);
// When starting up in an unknown state, we optimistically rely on ping()
// and the fast retry loop while disconnected. Slow health checks while connected
}, [isElectrumDisabled]);
// When starting up in an unknown state, we optimistically rely on ping(); on resume we
// call ensureElectrumConnection() (reconnect if the socket died in the background).
// Fast retry loop while disconnected uses ping only. Slow health checks while connected
// run only from WalletsList when that screen is focused (saves idle battery).
useEffect(() => {

View File

@ -34,6 +34,7 @@ import loc, { formatBalance } from '../../loc';
import { Chain } from '../../models/bitcoinUnits';
import ActionSheet from '../ActionSheet';
import { useStorage } from '../../hooks/context/useStorage';
import { WalletTransactionsStatus } from '../../components/Context/StorageProvider';
import WatchOnlyWarning from '../../components/WatchOnlyWarning';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
@ -61,7 +62,7 @@ type WalletTransactionsProps = NativeStackScreenProps<DetailViewStackParamList,
type TransactionListItem = Transaction & { type: 'transaction' | 'header' };
const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { route: WalletTransactionsRouteProps }) => {
const { wallets, saveToDisk } = useStorage();
const { wallets, saveToDisk, walletTransactionUpdateStatus } = useStorage();
const { registerTransactionsHandler, unregisterTransactionsHandler } = useMenuElements();
const { isBiometricUseCapableAndEnabled } = useBiometrics();
const { direction } = useLocale();
@ -85,6 +86,8 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
return wallet.type === WatchOnlyWallet.type && (wallet as any).isWatchOnlyWarningVisible;
});
const MAX_FAILURES = 3;
const isUpstreamWalletRefreshBusy =
walletTransactionUpdateStatus === WalletTransactionsStatus.ALL || walletTransactionUpdateStatus === walletID;
const flatListRef = useRef<FlatList<Transaction>>(null);
const headerRef = useRef<View>(null);
const [headerHeight, setHeaderHeight] = useState(0);
@ -197,7 +200,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
const refreshTransactions = useCallback(
async (isManualRefresh = false) => {
console.debug('refreshTransactions, ', wallet.getLabel());
if (isElectrumDisabled || isLoading) return;
if (isElectrumDisabled || isLoading || isUpstreamWalletRefreshBusy) return;
const MIN_REFRESH_INTERVAL = 5000; // 5 seconds
if (!isManualRefresh && lastFetchTimestamp !== 0 && Date.now() - lastFetchTimestamp < MIN_REFRESH_INTERVAL) {
@ -257,14 +260,14 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
setIsLoading(false);
}
},
[wallet, isElectrumDisabled, isLoading, saveToDisk, pageSize, lastFetchTimestamp, fetchFailures],
[wallet, isElectrumDisabled, isLoading, isUpstreamWalletRefreshBusy, saveToDisk, pageSize, lastFetchTimestamp, fetchFailures],
);
useEffect(() => {
if (lastFetchTimestamp === 0 && !isLoading && !isElectrumDisabled) {
if (lastFetchTimestamp === 0 && !isLoading && !isElectrumDisabled && !isUpstreamWalletRefreshBusy) {
refreshTransactions(false).catch(console.error);
}
}, [wallet, isElectrumDisabled, isLoading, refreshTransactions, lastFetchTimestamp]);
}, [wallet, isElectrumDisabled, isLoading, isUpstreamWalletRefreshBusy, refreshTransactions, lastFetchTimestamp]);
const isLightning = useCallback((): boolean => wallet.chain === Chain.OFFCHAIN || false, [wallet]);
const renderListFooterComponent = () => {

View File

@ -20,6 +20,7 @@ import { ConnectionPollContext } from '../../navigation/ConnectionPollContext';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import { useStorage } from '../../hooks/context/useStorage';
import { WalletTransactionsStatus } from '../../components/Context/StorageProvider';
import TotalWalletsBalance from '../../components/TotalWalletsBalance';
import { useSettings } from '../../hooks/context/useSettings';
import useMenuElements from '../../hooks/useMenuElements';
@ -105,7 +106,8 @@ const WalletsList: React.FC = () => {
const connectionPoll = useContext(ConnectionPollContext);
const currentWalletIndex = useRef<number>(0);
const { registerTransactionsHandler, unregisterTransactionsHandler } = useMenuElements();
const { wallets, getTransactions, refreshAllWalletTransactions } = useStorage();
const { wallets, getTransactions, refreshAllWalletTransactions, walletTransactionUpdateStatus } = useStorage();
const isGlobalTransactionRefreshBusy = walletTransactionUpdateStatus !== WalletTransactionsStatus.NONE;
const { isTotalBalanceEnabled, isElectrumDisabled } = useSettings();
const { width } = useWindowDimensions();
const { colors, scanImage } = useTheme();
@ -171,10 +173,13 @@ const WalletsList: React.FC = () => {
}, []);
const onRefresh = useCallback(() => {
if (isGlobalTransactionRefreshBusy) {
return Promise.resolve();
}
console.debug('WalletsList onRefresh');
return refreshTransactions();
// Optimized for Mac option doesn't like RN Refresh component. Menu Elements now handles it for macOS
}, [refreshTransactions]);
}, [refreshTransactions, isGlobalTransactionRefreshBusy]);
useEffect(() => {
const screenKey = route.name;
@ -466,7 +471,10 @@ const WalletsList: React.FC = () => {
return `${item}${index}`;
}, []);
const refreshProps = isDesktop || isElectrumDisabled ? {} : { refreshing: isLoading, onRefresh };
const refreshProps = useMemo(
() => (isDesktop || isElectrumDisabled ? {} : { refreshing: isLoading, onRefresh }),
[isElectrumDisabled, isLoading, onRefresh],
);
const sections: SectionData[] = useMemo(() => {
// On large screens, only show transactions section