Skip to content

Conversation

@paullinator
Copy link
Member

@paullinator paullinator commented Jan 22, 2026

Change Server Bug Fixes

This document explains the fixes for three known issues with the change server integration in edge-core-js.

Issue 1: Sending 0 Checkpoint to the Change Server

Problem

The core was sending a 0 (or undefined) checkpoint to the change server even if it had received a higher-number checkpoint in a previous notification or subscription response.

This happened because when onSubscribeAddresses was called by the currency engine with plain string addresses (rather than objects with checkpoints), the code would create new subscription objects with checkpoint: undefined, discarding any checkpoint that had been previously received and stored in the wallet state.

Root Cause

In currency-wallet-callbacks.ts, the onSubscribeAddresses callback was mapping addresses to subscriptions without checking if an existing subscription (with a valid checkpoint) already existed for that address:

// Before fix - always created new subscription with undefined checkpoint
const subscribedAddresses = paramsOrAddresses.map(param =>
  typeof param === 'string'
    ? { address: param, checkpoint: undefined }
    : param
)

Fix

The fix looks up any existing subscription for the address and preserves its checkpoint:

// After fix - preserves existing checkpoint
const subscribedAddresses = paramsOrAddresses.map(param => {
  if (typeof param === 'string') {
    const existing = existingSubscriptions.find(
      sub => sub.address === param
    )
    return {
      address: param,
      checkpoint: existing?.checkpoint
    }
  }
  return param
})

Files Changed

  • src/core/currency/wallet/currency-wallet-callbacks.ts
  • src/core/currency/wallet/currency-wallet-pixie.ts

Issue 2: Failing to Re-establish Change Server Connections

Problem

The core was not properly re-establishing the connection to the change server after a disconnect. Wallets would remain in a disconnected state and would not receive change notifications.

Root Cause

Two issues contributed to this problem:

  1. Reconnection logic in wrong event handler: The WebSocket reconnection logic was in the error event handler instead of the close event handler. Since the error event doesn't always fire (and the close event always fires when a connection is lost), reconnection was unreliable.

  2. 'reconnecting' status not handled: After a disconnect, wallets would be set to 'reconnecting' status by the handleDisconnect callback. However, the subscription filters in currency-pixie.ts only checked for 'subscribing' and 'resubscribing' statuses, so wallets in 'reconnecting' status would never be re-subscribed.

Fix

  1. Move reconnection to close handler: The reconnection logic was moved from the error handler to the close handler, with a closing flag to prevent reconnection when the connection was intentionally closed:
// In change-server-connection.ts
let closing = false

ws.addEventListener('close', () => {
  // ... handle disconnect ...

  // Reconnect after 5 seconds, unless intentionally closed:
  if (!closing) {
    setTimeout(() => {
      makeWs()
    }, 5000)
  }
})

// The close() method now sets the flag:
close() {
  closing = true
  ws.close()
}
  1. Add 'reconnecting' to subscription filters: The subscription filters now include 'reconnecting' status:
// In currency-pixie.ts
const filteredWallets = supportedWallets.filter(([, wallet]) =>
  wallet.changeServiceSubscriptions.some(
    subscription =>
      subscription.status === 'subscribing' ||
      subscription.status === 'resubscribing' ||
      subscription.status === 'reconnecting'  // Added
  )
)
  1. Fix error handling for batch subscriptions: When a batch subscription fails, the error handler now returns a failure result for each subscription in the batch (rather than a single [0]), ensuring proper status tracking:
// Before: return [0] as SubscribeResult[]
// After:
return subscribeParams.map(() => 0 as SubscribeResult)

Files Changed

  • src/core/currency/change-server-connection.ts
  • src/core/currency/currency-pixie.ts

Issue 3: Not Properly Saving Checkpoints

Problem

The core was not properly saving checkpoints received from the change server. This meant that after an app restart, the wallet would lose track of its synchronization progress and might receive duplicate notifications or miss changes.

Root Cause

Two issues:

  1. Checkpoints not persisted after sync: When a wallet completed a sync cycle and transitioned to 'listening' status, the updated checkpoint was stored in Redux state but not persisted to disk.

  2. Case-sensitive address comparison: When preserving existing checkpoints, the address comparison was case-sensitive. Since blockchain addresses can be represented in different cases (especially for EVM chains with checksummed addresses), this could cause checkpoint loss when the engine provided addresses in a different case than what was stored.

Fix

  1. Persist checkpoints after sync: The syncNetworkUpdate pixie now saves checkpoints to disk after each successful sync:
// In currency-wallet-pixie.ts - syncNetworkUpdate
// After dispatching the listening status update:
const subscribedAddresses = walletState.changeServiceSubscriptions.map(
  subscription => ({
    address: subscription.address,
    checkpoint: subscription.checkpoint
  })
)
await saveSeenTxCheckpointFile(
  input,
  walletState.seenTxCheckpoint ?? undefined,
  subscribedAddresses
)
  1. Case-insensitive address comparison: The checkpoint preservation logic now uses case-insensitive comparison:
// In currency-wallet-callbacks.ts - onSubscribeAddresses
const existing = existingSubscriptions.find(
  sub => sub.address.toLowerCase() === address.toLowerCase()
)
  1. Unified handling for string and object params: The code now handles both string addresses and EdgeSubscribedAddress objects consistently, preferring any explicit checkpoint from the param but falling back to an existing checkpoint:
const address = typeof param === 'string' ? param : param.address
const explicitCheckpoint = typeof param === 'object' ? param.checkpoint : undefined
return {
  address,
  checkpoint: explicitCheckpoint ?? existing?.checkpoint
}

Files Changed

  • src/core/currency/wallet/currency-wallet-callbacks.ts
  • src/core/currency/wallet/currency-wallet-pixie.ts

Summary

Issue Commit Key Change
Sending 0 checkpoint d80434a0 Preserve existing checkpoint when re-subscribing addresses
Connection re-establishment 055200fa Move reconnect to close handler, handle 'reconnecting' status
Checkpoint persistence d80434a0, 8a79c7f0 Save checkpoints to disk, use case-insensitive address comparison

These fixes ensure that:

  1. Checkpoints are preserved in memory when engines call onSubscribeAddresses
  2. The WebSocket connection reliably reconnects after any type of disconnect
  3. Wallets in 'reconnecting' status are properly re-subscribed
  4. Checkpoints are persisted to disk and survive app restarts
  5. Address comparison works correctly regardless of case differences

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Description

none

Note

Strengthens change-server reliability and checkpoint handling.

  • Move reconnect logic to WebSocket close with a closing guard; stop auto-reconnect on intentional close() (change-server-connection.ts)
  • Include reconnecting in subscription filters and status updates; handle batch subscribe failures by returning a result per item (currency-pixie.ts)
  • Preserve existing checkpoints on onSubscribeAddresses using case-insensitive address matching; persist subscribed addresses and checkpoints (currency-wallet-callbacks.ts)
  • After syncNetwork, set subscriptions to listening and persist updated checkpoints to disk (currency-wallet-pixie.ts)

Written by Cursor Bugbot for commit 0e0331b. This will update automatically on new commits. Configure here.


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.
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.
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.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

if (!closing) {
setTimeout(() => {
makeWs()
}, 5000)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reconnection timeout not cancelled when close() is called

Medium Severity

The closing flag is only checked when the close event fires, not when the scheduled reconnection callback executes. If a WebSocket disconnects unexpectedly and then close() is called before the 5-second reconnect timeout fires, the reconnection will still happen because the setTimeout callback calls makeWs() unconditionally. The callback needs to check if (!closing) before calling makeWs().

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants