fix(wallet): stop believing a socket that says OPEN after a trip to the wallet - #36
Merged
Merged
Conversation
…he wallet Coming back from MetaMask, the page sat on "Connecting Wallet..." for several seconds after the user had already approved. Two causes, both ours. 1. The wake trusted `relayer.connected`, which is nothing but the WebSocket's own `readyState === 1`. Android suspends the TCP connection underneath a socket without telling it, so it keeps reporting OPEN while the wallet's approval sits on the relay waiting for a client that thinks it needs nothing. Nothing in @walletconnect/core catches this from a browser: the ping-based liveness check (`startPingTimeout`) is gated on `isNode()`, and the heartbeat's own reconnect only fires on `!connected`. And our `wakeRelay` returned early on exactly the same flag. So on return, everyone looked at a dead socket that said OPEN and did nothing until Chrome's TCP stack gave up. Now, when the tab has been hidden for at least BACKGROUND_STINT_MS (1s -- a trip to a wallet is never shorter, a flick between desktop tabs usually is), `readyState` is not consulted and the transport is restarted outright. `restartTransport()` tears the socket down, dials again, re-subscribes every topic, and its subscriber then calls `batchFetchMessages` -- which is the fetch of whatever the wallet published while we were dead. A socket that was in fact healthy pays one reconnect, well under a second; one that was not pays nothing it did not already owe. 2. The wake never ran for the connect round-trip at all. `wallet.provider` comes from AppKit's `ProviderController.setProvider`, which runs on connection -- so during the connect itself, the first trip every user makes, it was undefined and `useRelayWake` had nothing to act on. The socket the approval arrives over lives on AppKit's UniversalProvider, which exists from the moment the chooser opens; WalletSigner now asks AppKit for it and wakes that when there is no session provider yet. Also: the stuck-wallet hint names the likely cause of MetaMask wedging on its splash screen EVERY time -- Android restricting the wallet in the background, so it is suspended or killed on every switch and cannot handle the request it is resumed for. The fix is the user's to make (Settings > Apps > MetaMask > Battery > Unrestricted, and the same for the browser); it is the setting the mobile app always needed for the same prompts, and a web page cannot set it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013BpqXrkEPA9odTyRdK5Mnx
ehsan6sha
added a commit
to functionland/docs
that referenced
this pull request
Sep 4, 2026
…time, and shorten the wait (#179) Two updates to the wallet hand-off guidance. The "give the page a few seconds" note now says "a second or two". As of functionland/fxblox-web#36 the app restarts its relay socket the moment the tab comes back from a wallet rather than trusting a socket that only claims to be open, so the approval is fetched at once instead of after Chrome's TCP stack notices. The advice not to tap again or reload in the meantime stands. The stuck-splash-screen tip now says what to do when it happens EVERY time: Android is restricting MetaMask in the background, so it is suspended or killed on every switch away and cannot handle the request it is resumed for. Settings > Apps > MetaMask > Battery > Unrestricted, and the same for the browser. This is the setting the mobile FxBlox app always needed for the same prompts -- the legacy section of this page has said so for a long time -- and the web app is subject to it in exactly the same way. A web page cannot change it for the user, so the page has to tell them. Same line added to the troubleshooting list and to Switch Chain Troubleshooting. Claude-Session: https://claude.ai/code/session_013BpqXrkEPA9odTyRdK5Mnx Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
ehsan6sha
added a commit
that referenced
this pull request
Sep 4, 2026
…ke the hand-off measurable (#38) The reporter still sees about five seconds of "Connecting Wallet..." after approving in MetaMask, with #36 deployed. Two things in response. 1. The reconnect no longer waits for a dead socket to say goodbye. #36 replaced a socket that only claimed to be OPEN by calling `restartTransport()`. Read further, that is the polite path: `resetTransport` -> `transportDisconnect` -> `provider.disconnect()` under a 2 s timeout -- two seconds spent waiting for a close handshake that a TCP connection Android suspended will never complete, before a single byte of the reconnect is sent. The relayer has a faster path, and it is the one IT takes when a socket's `close` event fires on its own: `onProviderDisconnect()` -- stop the subscriber and dial a FRESH socket 100 ms later. The dead socket is abandoned rather than closed; `createProvider()` detaches its listeners first, so its eventual close reaches nobody. That is the truthful treatment of a socket that is, in fact, gone. The polite restart stays as the fallback if the fresh socket is not up within WAKE_TIMEOUT_MS, and the log says which path ran. (Checked while here: `batchFetchMessages`, with its 1 s sleep, has no call site in the shipped core. The relay pushes queued messages on `irn_batchSubscribe`, so nothing else on the path waits on purpose.) 2. The hand-off is now measurable from the phone. Every theory about the delay and the splash-screen hang has been argued from code reading, because the only evidence was console.log on a phone nobody had a debugger on. `wallet/diag.ts` routes the same lines into the clientLogger ring buffer, timestamped "+Nms since return" -- the moment every one of those seconds is counted from -- and the debug-mode banner now shares the buffer (plus the build sha) with one tap. Logged: tab hidden/visible with the hidden duration and whether a relay provider was even present; which reconnect path ran and how long it took; every AppKit connected/connecting/account/provider flip; on the sign tap, the relay state, the stored deep-link choice and Chrome's user-activation flag; when the request reached the relay; the exact URL of each hop and the activation flag at that moment; when the signature arrived. Always on -- the ring is 500 lines in memory -- so the one report that matters is not taken with it off. Claude-Session: https://claude.ai/code/session_013BpqXrkEPA9odTyRdK5Mnx Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Coming back from MetaMask, the page sat on Connecting Wallet… for several seconds after the user had already approved. Two causes, both ours.
1. The wake trusted a socket that only said it was open
relayer.connectedis nothing but the WebSocket's ownreadyState === 1:Android suspends the TCP connection underneath a socket without telling it, so it keeps reporting OPEN while the wallet's approval sits on the relay waiting for a client that thinks it needs nothing. Nothing in
@walletconnect/corecatches this from a browser — the ping-based liveness check (startPingTimeout) is gated onisNode(), and the heartbeat's own reconnect fires only on!connected. OurwakeRelayreturned early on the very same flag. So on return, everyone looked at a dead socket that said OPEN and did nothing until Chrome's TCP stack gave up. That was the delay.Now, when the tab has been hidden for at least
BACKGROUND_STINT_MS(1 s — a trip to a wallet is never shorter, a flick between desktop tabs usually is),readyStateis not consulted and the transport is restarted outright.restartTransport()tears the socket down, dials again, re-subscribes every topic, and its subscriber then callsbatchFetchMessages— which is precisely the fetch of whatever the wallet published while we were dead. A socket that was actually healthy pays one reconnect, well under a second; one that was not pays nothing it did not already owe.2. The wake never ran for the connect round-trip at all
wallet.providercomes from AppKit'sProviderController.setProvider, which runs on connection. So during the connect itself — the first trip every user makes, and the one in the report — it wasundefined, anduseRelayWakehad nothing to act on. The socket the approval arrives over lives on AppKit'sUniversalProvider, which exists from the moment the chooser opens.WalletSignernow asks AppKit for it (getUniversalProvider()) and wakes that when there is no session provider yet.The splash-screen hang
Not fixed by this PR, and I no longer believe a web page can fix it directly — two device tests ruled out user activation and URL shape. What it does is name the likely cause in the stuck-wallet hint: Android restricting MetaMask in the background, so it is suspended or killed on every switch away and cannot handle the request it is resumed for. The fix is the user's — Settings → Apps → MetaMask → Battery → Unrestricted, and the same for the browser. This is the exact setting the mobile FxBlox app always needed for these prompts (it is already in the legacy section of the docs); with it in place the wallet comes forward already showing the prompt. Same guidance goes to docs.fx.land in functionland/docs#179.
Testing
npm test --workspaces— 949 passing. New:wakeRelayrestarts a "connected" socket after a background stint but not otherwise; a genuinely-down socket still takes the polite path; a relayer withoutrestartTransportis left alone; a failed restart is swallowed.useRelayWakerestarts after ≥ 1 s hidden, leaves an open socket alone after a shorter flick, and ignores avisiblewith no precedinghidden.npm run typecheck --workspaces,npm run lintclean.🤖 Generated with Claude Code
https://claude.ai/code/session_013BpqXrkEPA9odTyRdK5Mnx