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
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:
parent
d09bd69b96
commit
8195855f05
@ -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 = () => {
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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 = () => {
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user