Compare commits

...

4 Commits

Author SHA1 Message Date
Overtorment
8d5ae31f27 REF: blue electrum 2026-05-26 21:50:30 +01:00
Overtorment
e920e4d72b REF: blue electrum 2026-05-26 21:16:42 +01:00
Overtorment
0ceef5fa0a REF: blue electrum 2026-05-26 21:01:27 +01:00
Overtorment
aa406ae181 REF: blue electrum 2026-05-26 19:08:53 +01:00
24 changed files with 420 additions and 169 deletions

View File

@ -100,24 +100,84 @@ export const suggestedServers: Peer[] = hardcodedPeers.map(peer => ({
}));
let mainClient: typeof ElectrumClient | undefined;
let mainConnected: boolean = false;
let wasConnectedAtLeastOnce: boolean = false;
let serverName: string | false = false;
let disableBatching: boolean = false;
let connectionAttempt: number = 0;
let currentPeerIndex = hardcodedPeers.findIndex(peer => peer.host === defaultPeer.host && peer.ssl === defaultPeer.ssl);
if (currentPeerIndex < 0) currentPeerIndex = 0;
let latestBlock: { height: number; time: number } | { height: undefined; time: undefined } = { height: undefined, time: undefined };
const WAIT_TILL_CONNECTED_TICK_MS = 100;
/** After at least one successful Electrum session: wall ~30s before timeout (slow reconnect). */
const WAIT_TILL_CONNECTED_MAX_TICKS_AFTER_FIRST_CONNECT = 300;
/** First-ever connect: wall ~60s before timeout (cold start / slow TLS / flaky network). */
const WAIT_TILL_CONNECTED_MAX_TICKS_NEVER_CONNECTED = 600;
// --- Single source of truth for connection liveness -----------------------------
// We previously tracked `mainConnected` (boolean) separately from the client's own
// `mainClient.status`. They drifted on iOS suspend/resume: a transient `ping()`
// failure cleared the flag while the socket was still alive, then `waitTillConnected`
// blocked for ~30s on the stale flag and surfaced a false network-error alert. The
// state machine + `ensureConnected()` below is the only place that mutates the
// connection lifecycle, and UI is driven by subscribing to state changes.
/** Max wall time for one `waitTillConnected` wait (ms); derived from ticks above for callers (e.g. refresh fetch race). */
export const WAIT_TILL_CONNECTED_MAX_WALL_MS_AFTER_FIRST = WAIT_TILL_CONNECTED_MAX_TICKS_AFTER_FIRST_CONNECT * WAIT_TILL_CONNECTED_TICK_MS;
export const WAIT_TILL_CONNECTED_MAX_WALL_MS_NEVER = WAIT_TILL_CONNECTED_MAX_TICKS_NEVER_CONNECTED * WAIT_TILL_CONNECTED_TICK_MS;
export type ConnectionState = 'disabled' | 'disconnected' | 'connecting' | 'connected';
let connState: ConnectionState = 'disconnected';
type ConnectionListener = (state: ConnectionState) => void;
const connectionListeners = new Set<ConnectionListener>();
function setConnectionState(next: ConnectionState): void {
if (connState === next) return;
connState = next;
for (const l of connectionListeners) {
try {
l(next);
} catch (e) {
console.warn('connection listener threw:', e);
}
}
}
/** Current connection state for UI. */
export function getConnectionState(): ConnectionState {
return connState;
}
/** Subscribe to state changes. Returns an unsubscribe function. */
export function subscribeConnectionState(listener: ConnectionListener): () => void {
connectionListeners.add(listener);
return () => {
connectionListeners.delete(listener);
};
}
/** Convenience: `true` iff a usable Electrum connection is currently believed to exist. */
export function isConnected(): boolean {
return connState === 'connected';
}
// --- Connection lifecycle internals ---------------------------------------------
/** One liveness check (`server_ping`) wall-time before giving up and marking the socket dead. */
const PING_TIMEOUT_MS = 5_000;
/** One full connect attempt (TLS + `server_version` handshake) wall-time before retrying. */
const CONNECT_ATTEMPT_TIMEOUT_MS = 10_000;
/** Reconnect attempts inside a single `ensureConnected()` call before declaring failure. */
const CONNECT_MAX_ATTEMPTS = 5;
/** Backoff between attempts to avoid hammering a flaky server. */
const CONNECT_BACKOFF_MS = 500;
/** Delay before the auto-reconnect triggered by a live-socket `onError`. Onions are slower. */
const RECONNECT_ONION_DELAY_MS = 4_000;
const RECONNECT_TCP_DELAY_MS = 500;
/** Max wall time one `ensureConnected()` call may take when no live socket exists. */
export const ENSURE_CONNECTED_MAX_WALL_MS =
CONNECT_MAX_ATTEMPTS * CONNECT_ATTEMPT_TIMEOUT_MS + (CONNECT_MAX_ATTEMPTS - 1) * CONNECT_BACKOFF_MS;
/** Coalesces concurrent `ensureConnected()` callers — at most one connect attempt at a time. */
let ensureInFlight: Promise<boolean> | null = null;
/** If any coalesced caller asked for the failure alert, honour it once the in-flight attempt finishes. */
let ensureInFlightShowAlert = false;
/**
* Bumps every time the caller asks us to abandon the current connection
* (`forceDisconnect()` or user disabling Electrum). In-flight `ensureConnected()`
* checks this between attempts so it can bail out promptly instead of racing back
* to `connected` after a disconnect was requested.
*/
let disconnectGeneration = 0;
const txhashHeightCache: Record<string, number> = {};
let _realm: Realm | undefined;
@ -259,48 +319,93 @@ async function getSavedPeer(): Promise<Peer | null> {
}
}
export async function connectMain(): Promise<void> {
if (await isDisabled()) {
console.log('Electrum connection disabled by user. Skipping connectMain call');
return;
}
/** Resolve to the peer this attempt should target (preferred saved peer, or rotate hardcoded list). */
async function pickPeer(): Promise<Peer> {
let usingPeer = getNextPeer();
const savedPeer = await getSavedPeer();
if (savedPeer && savedPeer.host && (savedPeer.tcp || savedPeer.ssl)) {
usingPeer = savedPeer;
}
return usingPeer;
}
function scheduleReconnectFromClient(client: typeof ElectrumClient, usingPeer: Peer, reason: string): void {
if (connState !== 'connected' || mainClient !== client) return;
console.log(`scheduling Electrum reconnect after ${reason}`);
try {
// Also neutralises electrum-client's own timers/reconnect hooks for this instance.
client.close();
} catch {}
if (mainClient === client) mainClient = undefined;
setConnectionState('disconnected');
const delay = usingPeer.host.endsWith('.onion') ? RECONNECT_ONION_DELAY_MS : RECONNECT_TCP_DELAY_MS;
const generationAtSchedule = disconnectGeneration;
setTimeout(() => {
if (generationAtSchedule !== disconnectGeneration) return;
// eslint-disable-next-line @typescript-eslint/no-use-before-define -- defined later in file
ensureConnected().catch(() => {
/* ensureConnected never throws, but be defensive */
});
}, delay);
}
/**
* One connect attempt: build a fresh `ElectrumClient`, run the version handshake,
* subscribe to headers. No retries, no UI side effects. Returns the peer used
* (for caller-side telemetry/alerts) and whether the attempt succeeded.
*/
async function attemptConnectOnce(): Promise<{ ok: boolean; peer: Peer }> {
const usingPeer = await pickPeer();
console.log('Using peer:', JSON.stringify(usingPeer));
// Drop any prior client before allocating a new one. Closing also neutralises
// electrum-client's internal `reconnect()` loop on the old instance.
if (mainClient) {
try {
mainClient.close();
} catch {}
mainClient = undefined;
}
try {
console.log('begin connection:', JSON.stringify(usingPeer));
mainClient = new ElectrumClient(net, tls, usingPeer.ssl || usingPeer.tcp, usingPeer.host, usingPeer.ssl ? 'tls' : 'tcp');
const client = new ElectrumClient(net, tls, usingPeer.ssl || usingPeer.tcp, usingPeer.host, usingPeer.ssl ? 'tls' : 'tcp');
mainClient = client;
mainClient.onError = function (e: { message: string }) {
// Live-socket errors after a successful handshake: schedule a single
// `ensureConnected()` (deduped). Errors during this attempt's own handshake
// are caught below — we must not double-handle them here.
client.onError = function (e: { message: string }) {
console.log('electrum mainClient.onError():', e.message);
if (mainConnected) {
// 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 = undefined;
mainConnected = false;
// dropping `mainConnected` flag ensures there wont be reconnection race condition if several
// errors triggered
console.log('reconnecting after socket error');
setTimeout(connectMain, usingPeer.host.endsWith('.onion') ? 4000 : 500);
}
scheduleReconnectFromClient(client, usingPeer, 'socket error');
};
const ver = await mainClient.initElectrum({ client: 'bluewallet', version: '1.4' });
const ver = await Promise.race([
client.initElectrum(
{ client: 'bluewallet', version: '1.4' },
{
maxRetry: 0,
callback: () => scheduleReconnectFromClient(client, usingPeer, 'socket close'),
},
),
new Promise<never>((_resolve, reject) => setTimeout(() => reject(new Error('connect timeout')), CONNECT_ATTEMPT_TIMEOUT_MS)),
]);
if (mainClient !== client) {
// Caller raced `forceDisconnect()` while we were awaiting. Bail.
try {
client.close();
} catch {}
return { ok: false, peer: usingPeer };
}
if (ver && ver[0]) {
console.log('connected to ', ver);
serverName = ver[0];
mainConnected = true;
wasConnectedAtLeastOnce = true;
if (ver[0].startsWith('ElectrumPersonalServer') || ver[0].startsWith('electrs') || ver[0].startsWith('Fulcrum')) {
disableBatching = true;
// exeptions for versions:
const [electrumImplementation, electrumVersion] = ver[0].split(' ');
switch (electrumImplementation) {
case 'electrs':
@ -309,8 +414,6 @@ export async function connectMain(): Promise<void> {
}
break;
case 'electrs-esplora':
// its a different one, and it does NOT support batching
// nop
break;
case 'Fulcrum':
if (semVerToInt(electrumVersion) >= semVerToInt('1.9.0')) {
@ -319,36 +422,156 @@ export async function connectMain(): Promise<void> {
break;
}
}
const header = await mainClient.blockchainHeaders_subscribe();
const header = await client.blockchainHeaders_subscribe();
if (header && header.height) {
latestBlock = {
height: header.height,
time: Math.floor(+new Date() / 1000),
};
}
// AsyncStorage.setItem(storageKey, JSON.stringify(peers)); TODO: refactor
return { ok: true, peer: usingPeer };
}
return { ok: false, peer: usingPeer };
} catch (e) {
mainConnected = false;
console.log('bad connection:', JSON.stringify(usingPeer), e);
mainClient?.close();
mainClient = undefined;
if (mainClient) {
try {
mainClient.close();
} catch {}
mainClient = undefined;
}
return { ok: false, peer: usingPeer };
}
}
/** Single liveness check on the current `mainClient`, bounded by `PING_TIMEOUT_MS`. */
async function pingWithTimeout(timeoutMs: number = PING_TIMEOUT_MS): Promise<boolean> {
if (!mainClient) return false;
const client = mainClient;
try {
await Promise.race([
client.server_ping(),
new Promise<never>((_resolve, reject) => setTimeout(() => reject(new Error('ping timeout')), timeoutMs)),
]);
return mainClient === client; // server replied AND client wasn't swapped while we waited
} catch {
return false;
}
}
export type EnsureConnectedOptions = {
/**
* Show the legacy "couldn't connect" alert (Try again / Reset / Cancel) on failure.
* Used by initial bootstrap (`SettingsProvider` re-enabling Electrum) and the manual
* help alert. Off-hot-path callers (refresh, broadcast, etc.) should leave this false
* and surface their own UI.
*/
showAlertOnFailure?: boolean;
};
/**
* Make sure a usable Electrum connection exists, healing if needed.
*
* - If we already think we're connected, run one fast `ping` to verify. If the ping
* succeeds, we're done. If it fails the client is torn down and we fall through
* to a reconnect.
* - Otherwise run up to `CONNECT_MAX_ATTEMPTS` connect attempts (each with its own
* timeout + backoff).
*
* Concurrent callers share the same in-flight promise there is at most one connect
* attempt at a time per process. This replaces the old `mainConnected`-flag-polling
* `waitTillConnected()`, which could block ~30s on a stale flag while the socket was
* still alive.
*/
export async function ensureConnected(opts: EnsureConnectedOptions = {}): Promise<boolean> {
const { showAlertOnFailure = false } = opts;
if (await isDisabled()) {
setConnectionState('disabled');
return false;
}
if (!mainConnected) {
console.log('retry');
connectionAttempt = connectionAttempt + 1;
mainClient?.close();
mainClient = undefined;
if (connectionAttempt >= 5) {
// eslint-disable-next-line @typescript-eslint/no-use-before-define -- `presentNetworkErrorAlert` is defined below after `connectMain`
presentNetworkErrorAlert(usingPeer);
} else {
console.log('reconnection attempt #', connectionAttempt);
await new Promise(resolve => setTimeout(resolve, 500)); // sleep
return connectMain();
}
if (ensureInFlight) {
if (showAlertOnFailure) ensureInFlightShowAlert = true;
return ensureInFlight;
}
ensureInFlightShowAlert = showAlertOnFailure;
ensureInFlight = (async (): Promise<boolean> => {
const myGeneration = disconnectGeneration;
/** True iff the current generation no longer matches ours (i.e. `forceDisconnect()` ran). */
const aborted = (where: string): boolean => {
if (myGeneration === disconnectGeneration) return false;
console.log(`[BlueElectrum] ensureConnected aborted by forceDisconnect at ${where} (gen ${myGeneration}${disconnectGeneration})`);
return true;
};
let lastPeer: Peer | undefined;
try {
// Fast path: live ping on the existing client.
if (mainClient && connState === 'connected') {
if (await pingWithTimeout()) {
if (aborted('post-ping')) {
setConnectionState('disconnected');
return false;
}
return true;
}
// Stale socket. Tear it down so the attempt loop starts fresh.
try {
mainClient.close();
} catch {}
mainClient = undefined;
setConnectionState('disconnected');
}
if (aborted('pre-loop')) return false;
setConnectionState('connecting');
for (let i = 0; i < CONNECT_MAX_ATTEMPTS; i++) {
if (await isDisabled()) {
setConnectionState('disabled');
return false;
}
if (aborted(`attempt ${i} start`)) {
setConnectionState('disconnected');
return false;
}
const { ok, peer } = await attemptConnectOnce();
lastPeer = peer;
if (aborted(`attempt ${i} end`)) {
if (mainClient) {
try {
mainClient.close();
} catch {}
mainClient = undefined;
}
setConnectionState('disconnected');
return false;
}
if (ok) {
setConnectionState('connected');
return true;
}
if (i < CONNECT_MAX_ATTEMPTS - 1) {
await new Promise(resolve => setTimeout(resolve, CONNECT_BACKOFF_MS));
}
}
setConnectionState('disconnected');
if (ensureInFlightShowAlert) {
// eslint-disable-next-line @typescript-eslint/no-use-before-define -- defined later in file
presentNetworkErrorAlert(lastPeer);
}
return false;
} finally {
ensureInFlight = null;
ensureInFlightShowAlert = false;
}
})();
return ensureInFlight;
}
export async function presentResetToDefaultsAlert(): Promise<boolean> {
@ -431,10 +654,10 @@ async function presentNetworkErrorAlert(usingPeer?: Peer, allowRepeat = false) {
{
text: loc.wallets.list_tryagain,
onPress: () => {
connectionAttempt = 0;
mainClient?.close();
mainClient = undefined;
setTimeout(connectMain, 500);
forceDisconnect();
setTimeout(() => {
ensureConnected({ showAlertOnFailure: true }).catch(() => {});
}, 500);
},
style: 'default',
},
@ -443,10 +666,10 @@ async function presentNetworkErrorAlert(usingPeer?: Peer, allowRepeat = false) {
onPress: () => {
presentResetToDefaultsAlert().then(result => {
if (result) {
connectionAttempt = 0;
mainClient?.close();
mainClient = undefined;
setTimeout(connectMain, 500);
forceDisconnect();
setTimeout(() => {
ensureConnected({ showAlertOnFailure: true }).catch(() => {});
}, 500);
}
});
},
@ -455,9 +678,7 @@ async function presentNetworkErrorAlert(usingPeer?: Peer, allowRepeat = false) {
{
text: loc._.cancel,
onPress: () => {
connectionAttempt = 0;
mainClient?.close();
mainClient = undefined;
forceDisconnect();
},
style: 'cancel',
},
@ -524,12 +745,21 @@ export const getBalanceByAddress = async function (address: string): Promise<{ c
};
export const getConfig = async function () {
if (!mainClient) throw new Error('Electrum client is not connected');
if (!mainClient) {
return {
host: undefined,
port: undefined,
serverName: false as typeof serverName,
connected: connState === 'connected' ? 1 : 0,
};
}
return {
host: mainClient.host,
port: mainClient.port,
serverName,
connected: mainClient.timeLastCall !== 0 && mainClient.status,
// Drive UI "connected" indicator from the single state machine so the settings
// screen agrees with the wallets-list header pill and with `ensureConnected()`.
connected: connState === 'connected' ? 1 : 0,
};
};
@ -558,14 +788,24 @@ export const getMempoolTransactionsByAddress = async function (address: string):
return mainClient.blockchainScripthash_getMempool(uint8ArrayToHex(reversedHash));
};
export const ping = async function () {
try {
await mainClient.server_ping();
return true;
} catch (_) {}
mainConnected = false;
return false;
/**
* Read-only liveness probe. Does NOT trigger reconnects (use `ensureConnected()`
* for that). Updates the connection state machine to reflect the probe result so
* subscribers (UI pill, settings screen) stay in sync.
*
* - `true`: server replied within `PING_TIMEOUT_MS`.
* - `false`: client missing, timed out, or server errored.
*/
export const ping = async function (): Promise<boolean> {
if (await isDisabled()) return false;
const ok = await pingWithTimeout();
if (ok) {
// Heal stale `disconnected` state from a transient ping failure earlier.
if (connState !== 'connected') setConnectionState('connected');
} else if (connState === 'connected') {
setConnectionState('disconnected');
}
return ok;
};
// exported only to be used in unit tests
@ -1040,34 +1280,6 @@ export async function multiGetTransactionByTxid<T extends boolean>(
return ret;
}
export const waitTillConnected = async function (): Promise<boolean> {
let waitTillConnectedInterval: NodeJS.Timeout | undefined;
let retriesCounter = 0;
if (await isDisabled()) {
console.warn('Electrum connections disabled by user. waitTillConnected skipping...');
return false;
}
return new Promise(function (resolve, reject) {
waitTillConnectedInterval = setInterval(() => {
if (mainConnected) {
clearInterval(waitTillConnectedInterval);
return resolve(true);
}
retriesCounter += 1;
const maxTicks = wasConnectedAtLeastOnce
? WAIT_TILL_CONNECTED_MAX_TICKS_AFTER_FIRST_CONNECT
: WAIT_TILL_CONNECTED_MAX_TICKS_NEVER_CONNECTED;
if (retriesCounter >= maxTicks) {
clearInterval(waitTillConnectedInterval);
presentNetworkErrorAlert();
reject(new Error('Waiting for Electrum connection timeout'));
}
}, WAIT_TILL_CONNECTED_TICK_MS);
});
};
// Returns the value at a given percentile in a sorted numeric array.
// "Linear interpolation between closest ranks" method
function percentile(arr: number[], p: number) {
@ -1240,8 +1452,19 @@ export const testConnection = async function (host: string, tcpPort?: number, ss
return false;
};
/**
* Drop the current connection and tell any in-flight `ensureConnected()` to abort
* (so it doesn't race the disconnect by setting state back to `connected`).
*/
export const forceDisconnect = (): void => {
mainClient?.close();
disconnectGeneration += 1;
if (mainClient) {
try {
mainClient.close();
} catch {}
mainClient = undefined;
}
setConnectionState('disconnected');
};
export const setBatchingDisabled = () => {

View File

@ -220,7 +220,11 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = React.m
useEffect(() => {
if (walletsInitialized) {
isElectrumDisabled ? BlueElectrum.forceDisconnect() : BlueElectrum.connectMain();
if (isElectrumDisabled) {
BlueElectrum.forceDisconnect();
} else {
BlueElectrum.ensureConnected({ showAlertOnFailure: true });
}
}
}, [isElectrumDisabled, walletsInitialized]);

View File

@ -331,24 +331,21 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
setWalletTransactionUpdateStatus(WalletTransactionsStatus.ALL);
}
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();
// `ensureConnected()` ping-checks the existing socket and, only if needed,
// tears it down and reconnects. Replaces the old wait+ping+wait pattern
// which surfaced false "network error" alerts after iOS suspend/resume.
const connected = await BlueElectrum.ensureConnected();
if (!connected) {
console.log('[refreshAllWalletTransactions] could not establish Electrum connection, aborting refresh');
return;
}
console.debug('[refreshAllWalletTransactions] Connected to Electrum');
// Race only the post-connect work. `waitTillConnected` can take up to
// WAIT_TILL_CONNECTED_MAX_WALL_MS_NEVER (+ a second wait); starting the timer earlier caused refresh to abort
// while Electrum was still legitimately connecting.
const REFRESH_FETCH_PHASE_TIMEOUT_MS = Math.max(
120_000,
BlueElectrum.WAIT_TILL_CONNECTED_MAX_WALL_MS_NEVER + BlueElectrum.WAIT_TILL_CONNECTED_MAX_WALL_MS_AFTER_FIRST,
);
// Race only the post-connect work. We budget ample time so that a slow
// initial Electrum connection (cold start, slow TLS, flaky network) doesn't
// cause the fetch race to abort prematurely.
const REFRESH_FETCH_PHASE_TIMEOUT_MS = Math.max(120_000, BlueElectrum.ENSURE_CONNECTED_MAX_WALL_MS * 2);
const timeoutPromise = new Promise<never>(
(_resolve, reject) =>
(refreshTimeout = setTimeout(() => {
@ -418,7 +415,11 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
}
_lastTimeTriedToRefetchWallet[walletID] = Date.now();
await BlueElectrum.waitTillConnected();
const connected = await BlueElectrum.ensureConnected();
if (!connected) {
console.log('[fetchAndSaveWalletTransactions] could not establish Electrum connection, aborting');
return;
}
setWalletTransactionUpdateStatus(walletID);
const balanceStart = Date.now();

View File

@ -98,38 +98,51 @@ const DetailViewStackScreensStack = () => {
const { sizeClass } = useSizeClass();
const [electrumConnected, setElectrumConnected] = useState<boolean | null>(null);
// Probe connection health from the UI (e.g. WalletsList focus / 30s timer).
// BlueElectrum.ping() reflects the result into the shared connection state, which
// we observe via the subscription below — no need to set local state here.
const pollConnection = useCallback(async () => {
if (isElectrumDisabled) return;
const ok = await BlueElectrum.ping();
setElectrumConnected(ok);
await BlueElectrum.ping();
}, [isElectrumDisabled]);
// Mirror BlueElectrum's connection state into local UI state.
useEffect(() => {
if (isElectrumDisabled) {
setElectrumConnected(null);
return;
}
pollConnection();
}, [isElectrumDisabled, pollConnection]);
const sync = () => setElectrumConnected(BlueElectrum.isConnected());
sync();
const unsubscribe = BlueElectrum.subscribeConnectionState(sync);
// Kick off an initial probe so the header pill reflects reality after mount.
BlueElectrum.ping().catch(() => {});
return unsubscribe;
}, [isElectrumDisabled]);
// On foreground transition, proactively heal: ensureConnected() takes the fast
// ping path when the socket is alive (no-op) and rebuilds the connection only
// when needed. This replaces the old "ping → maybe show network alert" path that
// could surface a false alert after iOS suspend/resume.
useEffect(() => {
if (isElectrumDisabled) return;
const subscription = AppState.addEventListener('change', nextState => {
if (nextState === 'active') {
pollConnection();
BlueElectrum.ensureConnected().catch(() => {});
}
});
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
// run only from WalletsList when that screen is focused (saves idle battery).
}, [isElectrumDisabled]);
// While we believe we're disconnected, ask BlueElectrum to keep trying to
// reconnect (silently — the red "Not connected" pill is the only UI signal).
useEffect(() => {
if (isElectrumDisabled || electrumConnected !== false) return;
const interval = setInterval(pollConnection, 3000);
const interval = setInterval(() => {
BlueElectrum.ensureConnected().catch(() => {});
}, 3000);
return () => clearInterval(interval);
}, [isElectrumDisabled, electrumConnected, pollConnection]);
}, [isElectrumDisabled, electrumConnected]);
const connectionPollContextValue = useMemo(() => ({ pollConnection }), [pollConnection]);

View File

@ -98,8 +98,9 @@ const Broadcast: React.FC = () => {
Keyboard.dismiss();
setBroadcastResult(BROADCAST_RESULT.pending);
try {
await BlueElectrum.ping();
await BlueElectrum.waitTillConnected();
if (!(await BlueElectrum.ensureConnected())) {
throw new Error(loc.errors.network);
}
const walletObj = new HDSegwitBech32Wallet();
if (txHex) {
const result = await walletObj.broadcastTx(txHex);

View File

@ -247,8 +247,9 @@ const Confirm: React.FC = () => {
};
const broadcastTransaction = async (transaction: string) => {
await BlueElectrum.ping();
await BlueElectrum.waitTillConnected();
if (!(await BlueElectrum.ensureConnected())) {
throw new Error(loc.errors.network);
}
const result = await wallet.broadcastTx(transaction);
if (!result) {

View File

@ -147,8 +147,9 @@ const PsbtWithHardwareWallet = () => {
}
}
try {
await BlueElectrum.ping();
await BlueElectrum.waitTillConnected();
if (!(await BlueElectrum.ensureConnected())) {
throw new Error(loc.errors.network);
}
if (!txHex) {
setIsLoading(false);

View File

@ -127,8 +127,7 @@ export default class SelfTest extends Component {
//
if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
await BlueElectrum.ping();
await BlueElectrum.waitTillConnected();
if (!(await BlueElectrum.ensureConnected())) throw new Error('Could not connect to Electrum');
const addr4elect = '3GCvDBAktgQQtsbN6x5DYiQCMmgZ9Yk8BK';
const electrumBalance = await BlueElectrum.getBalanceByAddress(addr4elect);
if (electrumBalance.confirmed !== 51432)

View File

@ -78,8 +78,9 @@ export default class CPFP extends Component {
broadcast = () => {
this.setState({ isLoading: true }, async () => {
try {
await BlueElectrum.ping();
await BlueElectrum.waitTillConnected();
if (!(await BlueElectrum.ensureConnected())) {
throw new Error(loc.errors.network);
}
const result = await this.state.wallet.broadcastTx(this.state.txhex);
if (result) {
this.onSuccessBroadcast();

View File

@ -215,7 +215,9 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
let smthChanged = false;
try {
await BlueElectrum.waitTillConnected();
if (!(await BlueElectrum.ensureConnected())) {
throw new Error(loc.errors.network);
}
if (wallet.allowBIP47() && wallet.isBIP47Enabled() && 'fetchBIP47SenderPaymentCodes' in wallet) {
await wallet.fetchBIP47SenderPaymentCodes();
}

View File

@ -10,7 +10,7 @@ import { BlueDefaultTheme } from '../../components/themes';
jest.mock('../../blue_modules/BlueElectrum', () => {
return {
connectMain: jest.fn(),
ensureConnected: jest.fn().mockResolvedValue(true),
};
});

View File

@ -12,11 +12,8 @@ afterAll(() => {
beforeAll(async () => {
// awaiting for Electrum to be connected. For RN Electrum would naturally connect
// while app starts up, but for tests we need to wait for it
try {
await BlueElectrum.connectMain();
} catch (err) {
console.log('failed to connect to Electrum:', err);
process.exit(1);
if (!(await BlueElectrum.ensureConnected())) {
throw new Error('failed to connect to Electrum');
}
});

View File

@ -21,7 +21,9 @@ afterAll(async () => {
beforeAll(async () => {
// awaiting for Electrum to be connected. For RN Electrum would naturally connect
// while app starts up, but for tests we need to wait for it
await BlueElectrum.connectMain();
if (!(await BlueElectrum.ensureConnected())) {
throw new Error('failed to connect to Electrum');
}
});
describe('Bech32 Segwit HD (BIP84) with BIP47', () => {

View File

@ -17,7 +17,9 @@ afterAll(async () => {
beforeAll(async () => {
// awaiting for Electrum to be connected. For RN Electrum would naturally connect
// while app starts up, but for tests we need to wait for it
await BlueElectrum.connectMain();
if (!(await BlueElectrum.ensureConnected())) {
throw new Error('failed to connect to Electrum');
}
});
it('Legacy HD Breadwallet can fetch utxo, balance, and create transaction', async () => {

View File

@ -18,7 +18,9 @@ afterAll(async () => {
beforeAll(async () => {
// awaiting for Electrum to be connected. For RN Electrum would naturally connect
// while app starts up, but for tests we need to wait for it
await BlueElectrum.connectMain();
if (!(await BlueElectrum.ensureConnected())) {
throw new Error('failed to connect to Electrum');
}
});
let _cachedHdWallet = false;

View File

@ -13,7 +13,9 @@ afterAll(async () => {
beforeAll(async () => {
// awaiting for Electrum to be connected. For RN Electrum would naturally connect
// while app starts up, but for tests we need to wait for it
await BlueElectrum.connectMain();
if (!(await BlueElectrum.ensureConnected())) {
throw new Error('failed to connect to Electrum');
}
});
describe('Bech32 Segwit HD (BIP84)', () => {

View File

@ -14,11 +14,8 @@ afterAll(() => {
beforeAll(async () => {
// awaiting for Electrum to be connected. For RN Electrum would naturally connect
// while app starts up, but for tests we need to wait for it
try {
await BlueElectrum.connectMain();
} catch (Err) {
console.log('failed to connect to Electrum:', Err);
process.exit(2);
if (!(await BlueElectrum.ensureConnected())) {
throw new Error('failed to connect to Electrum');
}
});

View File

@ -30,7 +30,9 @@ afterAll(async () => {
beforeAll(async () => {
// awaiting for Electrum to be connected. For RN Electrum would naturally connect
// while app starts up, but for tests we need to wait for it
await BlueElectrum.connectMain();
if (!(await BlueElectrum.ensureConnected())) {
throw new Error('failed to connect to Electrum');
}
});
type THistoryItem = { action: 'progress'; data: string } | { action: 'wallet'; data: TWallet } | { action: 'password'; data: string };

View File

@ -15,7 +15,9 @@ afterAll(async () => {
beforeAll(async () => {
// awaiting for Electrum to be connected. For RN Electrum would naturally connect
// while app starts up, but for tests we need to wait for it
await BlueElectrum.connectMain();
if (!(await BlueElectrum.ensureConnected())) {
throw new Error('failed to connect to Electrum');
}
});
describe('LegacyWallet', function () {

View File

@ -13,11 +13,8 @@ afterAll(() => {
beforeAll(async () => {
// awaiting for Electrum to be connected. For RN Electrum would naturally connect
// while app starts up, but for tests we need to wait for it
try {
await BlueElectrum.connectMain();
} catch (Err) {
console.log('failed to connect to Electrum:', Err);
process.exit(2);
if (!(await BlueElectrum.ensureConnected())) {
throw new Error('failed to connect to Electrum');
}
});

View File

@ -13,7 +13,9 @@ afterAll(async () => {
beforeAll(async () => {
// awaiting for Electrum to be connected. For RN Electrum would naturally connect
// while app starts up, but for tests we need to wait for it
await BlueElectrum.connectMain();
if (!(await BlueElectrum.ensureConnected())) {
throw new Error('failed to connect to Electrum');
}
});
describe('Watch only wallet', () => {

View File

@ -9,7 +9,7 @@ jest.mock('../../blue_modules/currency', () => {
jest.mock('../../blue_modules/BlueElectrum', () => {
return {
connectMain: jest.fn(),
ensureConnected: jest.fn().mockResolvedValue(true),
};
});

View File

@ -6,7 +6,7 @@ import { LightningCustodianWallet } from '../../class/wallets/lightning-custodia
jest.mock('../../blue_modules/BlueElectrum', () => {
return {
connectMain: jest.fn(),
ensureConnected: jest.fn().mockResolvedValue(true),
};
});

View File

@ -8,7 +8,7 @@ import { WatchOnlyWallet } from '../../class/wallets/watch-only-wallet';
jest.mock('../../blue_modules/BlueElectrum', () => {
return {
connectMain: jest.fn(),
ensureConnected: jest.fn().mockResolvedValue(true),
};
});