From b6986324dd9f78a737e4d08dc56f83bd59f99695 Mon Sep 17 00:00:00 2001 From: Paul Puey Date: Fri, 2 Jan 2026 09:26:16 -0800 Subject: [PATCH 1/3] Preserve checkpoints when re-subscribing When onSubscribeAddresses is called with plain string addresses, look up any existing subscription for that address and preserve its checkpoint. This prevents sending a 0 checkpoint to the change server when the core already has a higher checkpoint. Also persist checkpoints to disk after sync completes. --- .../wallet/currency-wallet-callbacks.ts | 25 +++++++++++++------ .../currency/wallet/currency-wallet-pixie.ts | 14 ++++++++++- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/core/currency/wallet/currency-wallet-callbacks.ts b/src/core/currency/wallet/currency-wallet-callbacks.ts index 2e095ca3..1875cab5 100644 --- a/src/core/currency/wallet/currency-wallet-callbacks.ts +++ b/src/core/currency/wallet/currency-wallet-callbacks.ts @@ -296,15 +296,24 @@ export function makeCurrencyWalletCallbacks( onSubscribeAddresses( paramsOrAddresses: EdgeSubscribedAddress[] | string[] ) { + // Get existing subscriptions to preserve checkpoints + const existingSubscriptions = + input.props.walletState.changeServiceSubscriptions + const subscribedAddresses: EdgeSubscribedAddress[] = - paramsOrAddresses.map(param => - typeof param === 'string' - ? { - address: param, - checkpoint: undefined - } - : param - ) + paramsOrAddresses.map(param => { + if (typeof param === 'string') { + // Preserve existing checkpoint if we have one for this address + const existing = existingSubscriptions.find( + sub => sub.address === param + ) + return { + address: param, + checkpoint: existing?.checkpoint + } + } + return param + }) // Save subscribed addresses to disk (along with current checkpoint) saveSeenTxCheckpointFile( input, diff --git a/src/core/currency/wallet/currency-wallet-pixie.ts b/src/core/currency/wallet/currency-wallet-pixie.ts index 2b310532..ca229be8 100644 --- a/src/core/currency/wallet/currency-wallet-pixie.ts +++ b/src/core/currency/wallet/currency-wallet-pixie.ts @@ -45,6 +45,7 @@ import { loadSeenTxCheckpointFile, loadTokensFile, loadTxFileNames, + saveSeenTxCheckpointFile, writeTokensFile } from './currency-wallet-files' import { CurrencyWalletState, initialTokenIds } from './currency-wallet-reducer' @@ -273,7 +274,7 @@ export const walletPixie: TamePixie = combinePixies({ ), syncNetworkUpdate: filterPixie( - (_input: CurrencyWalletInput) => { + (input: CurrencyWalletInput) => { return { async update(props) { if (props.walletOutput == null) return @@ -308,6 +309,17 @@ export const walletPixie: TamePixie = combinePixies({ walletId: props.walletId } }) + // Persist the updated checkpoints to disk: + const subscribedAddresses = + walletState.changeServiceSubscriptions.map(subscription => ({ + address: subscription.address, + checkpoint: subscription.checkpoint + })) + await saveSeenTxCheckpointFile( + input, + walletState.seenTxCheckpoint ?? undefined, + subscribedAddresses + ) }, destroy() {} } From 7ab004ab730d4db1a4533f65df955418a77dffa1 Mon Sep 17 00:00:00 2001 From: Paul Puey Date: Fri, 2 Jan 2026 09:38:58 -0800 Subject: [PATCH 2/3] Fix change server reconnection Move reconnect logic from the error handler to the close handler so reconnection happens reliably after any disconnect. Add a closing flag to prevent reconnection when intentionally closed. Handle 'reconnecting' status in subscription filters so wallets are re-subscribed after the connection is re-established. --- src/core/currency/change-server-connection.ts | 12 ++++++++---- src/core/currency/currency-pixie.ts | 9 ++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/core/currency/change-server-connection.ts b/src/core/currency/change-server-connection.ts index c48b706a..953df279 100644 --- a/src/core/currency/change-server-connection.ts +++ b/src/core/currency/change-server-connection.ts @@ -26,6 +26,7 @@ export function connectChangeServer( callbacks: ChangeServerCallbacks ): ChangeServerConnection { let ws: WebSocket + let closing = false function makeWs(): void { ws = new WebSocket(url) ws.binaryType = 'arraybuffer' @@ -38,15 +39,17 @@ export function connectChangeServer( out.connected = false codec.handleClose() callbacks.handleDisconnect() + // Reconnect after 5 seconds, unless intentionally closed: + if (!closing) { + setTimeout(() => { + makeWs() + }, 5000) + } }) ws.addEventListener('error', errEvent => { console.error('changeServer websocket error:', errEvent) ws.close() - // Reconnect after 5 seconds: - setTimeout(() => { - makeWs() - }, 5000) }) ws.addEventListener('open', () => { @@ -87,6 +90,7 @@ export function connectChangeServer( }, close() { + closing = true ws.close() }, diff --git a/src/core/currency/currency-pixie.ts b/src/core/currency/currency-pixie.ts index c1aca16e..1a1514b5 100644 --- a/src/core/currency/currency-pixie.ts +++ b/src/core/currency/currency-pixie.ts @@ -226,7 +226,8 @@ export const currency: TamePixie = combinePixies({ wallet.changeServiceSubscriptions.some( subscription => subscription.status === 'subscribing' || - subscription.status === 'resubscribing' + subscription.status === 'resubscribing' || + subscription.status === 'reconnecting' ) ) const indexToWalletId: Array<{ @@ -292,7 +293,8 @@ export const currency: TamePixie = combinePixies({ .subscribe(subscribeParams) .catch(err => { input.props.log(`Failed to subscribe: ${String(err)}`) - return [0] as SubscribeResult[] + // Return failure result for each subscription in the batch: + return subscribeParams.map(() => 0 as SubscribeResult) }) results.push(...r) } @@ -347,7 +349,8 @@ export const currency: TamePixie = combinePixies({ subscription => subscription.status === 'subscribing' || subscription.status === 'subscribingSlowly' || - subscription.status === 'resubscribing' + subscription.status === 'resubscribing' || + subscription.status === 'reconnecting' ) .map(subscription => ({ ...subscription, From 0e0331b81465313e4487de209444ffcdaa29eb6c Mon Sep 17 00:00:00 2001 From: Paul Puey Date: Fri, 2 Jan 2026 10:39:42 -0800 Subject: [PATCH 3/3] Use case-insensitive address matching Use case-insensitive comparison when looking up existing checkpoints for addresses. This handles blockchains like EVM where addresses may be represented with different casing. Unify handling of string and object params, preferring explicit checkpoints from params but falling back to existing checkpoints. --- .../wallet/currency-wallet-callbacks.ts | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/core/currency/wallet/currency-wallet-callbacks.ts b/src/core/currency/wallet/currency-wallet-callbacks.ts index 1875cab5..b0b8b9a5 100644 --- a/src/core/currency/wallet/currency-wallet-callbacks.ts +++ b/src/core/currency/wallet/currency-wallet-callbacks.ts @@ -302,17 +302,19 @@ export function makeCurrencyWalletCallbacks( const subscribedAddresses: EdgeSubscribedAddress[] = paramsOrAddresses.map(param => { - if (typeof param === 'string') { - // Preserve existing checkpoint if we have one for this address - const existing = existingSubscriptions.find( - sub => sub.address === param - ) - return { - address: param, - checkpoint: existing?.checkpoint - } + const address = typeof param === 'string' ? param : param.address + // Preserve existing checkpoint if we have one for this address. + // Use case-insensitive comparison for blockchain addresses. + const existing = existingSubscriptions.find( + sub => sub.address.toLowerCase() === address.toLowerCase() + ) + // Prefer explicit checkpoint from param, fall back to existing + const explicitCheckpoint = + typeof param === 'object' ? param.checkpoint : undefined + return { + address, + checkpoint: explicitCheckpoint ?? existing?.checkpoint } - return param }) // Save subscribed addresses to disk (along with current checkpoint) saveSeenTxCheckpointFile(