Skip to content

fix(client): avoid interactive reconnect for remaining connectors - #70

Merged
chybisov merged 2 commits into
mainfrom
fix/passive-reconnect-connectors
Jul 27, 2026
Merged

fix(client): avoid interactive reconnect for remaining connectors#70
chybisov merged 2 commits into
mainfrom
fix/passive-reconnect-connectors

Conversation

@chybisov

@chybisov chybisov commented Jul 27, 2026

Copy link
Copy Markdown
Member

Follow-up to #67, which fixed this for UniSat only. Stacked on #69. GitHub retargets this to main automatically when #69 merges, but because #69 lands as a squash commit this branch needs a rebase at that point so its diff does not re-show #69's changes:

git rebase --onto origin/main c895e02 && git push --force-with-lease

Problem

reconnect() runs on app mount and calls connector.connect({ isReconnecting: true }). Four connectors ignored that flag and called provider.requestAccounts() unconditionally, which opens the wallet extension. Result: an unprompted wallet popup on every page load.

Connector Prompted on mount isAuthorized() trusted storage alone getAccounts() threw when locked
binance yes no yes
bitget yes yes yes
okx yes yes yes
onekey yes no yes

Two secondary issues fell out of the same audit:

  • bitget / okx isAuthorized() returned Boolean(storage['<id>.connected']) and never checked the extension — the exact weakness fix(client): avoid interactive UniSat reconnect #67 fixed for UniSat. A wallet that was uninstalled or locked still reported authorized.
  • All four getAccounts() passed accounts[0] into getAddressInfo() without a guard, throwing on a locked wallet. binance/onekey's isAuthorized() only returned false there by way of that throw hitting their catch.

Fix

Honour isReconnecting and verify authorization passively, matching the pattern already used by unisat, xverse (xverse.ts:183) and unhosted (unhosted.ts:200):

async connect({ isReconnecting } = {}) {
  
  if (!isReconnecting) {
    await provider.requestAccounts()
  }

Plus if (!address) return [] in each getAccounts(), and the passive account check in bitget/okx isAuthorized(). That last one reads like a logic inversion but is not: the account check now runs in both shim modes, where previously it was skipped entirely whenever shimDisconnect was set (the default). Behaviour is unchanged for shimDisconnect: false. OKX's getAccounts() fetched the public key before reading accounts, so that pair is reordered to avoid a wasted call on the empty path.

OKX: a self-inflicted permanent disconnect

Caught while reviewing this PR, and the reason for the second commit. okx.getChainId() is the only one of the five that derives the network from the first account, and OKX is one of two whose connect() catch calls await this.disconnect(). Combined with the return [] guard above, a reconnect against an account-less wallet did this:

requestAccountsCalls: 0                                          <- the fix works
storage: [["com.okex.wallet.bitcoin.disconnected", "true"]]      <- connected shim GONE
thrown: UserRejectedRequestError: ... Chain ID detection failed ...

getChainId() -> getAccounts() -> [] -> ConnectorChainIdDetectionError -> catch -> disconnect() persists disconnected and deletes connected. isAuthorized() then returns false on every later page load, so OKX never auto-reconnects again until the user clicks Connect by hand — silently, from one transient empty read.

The mechanism was pre-existing (dismissing the old prompt hit the same catch), but this PR turned a user-visible trigger into an invisible one. OKX now reads accounts before the chain id and bails early.

Error semantics

UniSat threw a bare Error for an account-less extension inside the try whose catch rewrites everything to UserRejectedRequestError — so a locked wallet surfaced to apps as "user rejected". All five connectors now wrap only requestAccounts() in that try, the one step a user can actually reject, and throw ConnectorNotConnectedError for the empty case. That narrowing is what keeps OKX's and Bitget's disconnect() cleanup scoped to real rejections instead of firing on any downstream failure.

Tests

New passiveReconnect.spec.ts runs the same four assertions against all four connectors via describe.each (16 tests). Verified against the unfixed code: 10 of the 16 fail, and exactly the expected ones — all four passive-reconnect and empty-accounts cases, plus is not authorized for bitget/okx only (binance/onekey already returned false, via the accidental throw).

A fifth shared case — reconnect against an account-less extension — asserts the throw type and that neither shim is disturbed. Verified against the previous commit: OKX fails it, the other three pass, which is exactly the blast radius described above. Two cases added to unisat.spec.ts pin the error semantics: account-less → ConnectorNotConnectedError, rejected prompt → UserRejectedRequestError.

pnpm test: 28 passed (client), 32 passed / 1 skipped (core).

Not included

reown also connects unconditionally (reown.ts:157, provider.connector.connect()), which opens the WalletConnect modal on mount. It is not a copy-paste of this fix and can't be covered by these stubs, so it is tracked separately.

Validation

pnpm check, pnpm check:types, pnpm check:circular-deps, pnpm knip:check, pnpm test all pass.

@changeset-bot

changeset-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d9359c6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@bigmi/client Patch
@bigmi/react Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Base automatically changed from fix/reconnect-hardening to main July 27, 2026 12:07
chybisov added 2 commits July 27, 2026 14:08
Binance, Bitget, OKX and OneKey called provider.requestAccounts()
unconditionally in connect(), so reconnect opened the wallet extension on
every page load. Honour isReconnecting and fall back to passive
getAccounts(), matching unisat, xverse and unhosted.

Bitget and OKX also short-circuited isAuthorized() on the shimDisconnect
storage flag without checking the extension still exposes an account, and
all four getAccounts() implementations fed accounts[0] into
getAddressInfo() unguarded, throwing when the wallet is locked.

UniSat's empty-accounts case now throws ConnectorNotConnectedError instead
of being laundered into UserRejectedRequestError by the catch-all — only
requestAccounts() can actually be rejected by a user.
okx.getChainId() derives the network from the first account, and okx's
connect() catch calls this.disconnect(). So a reconnect against a wallet
exposing no accounts threw ConnectorChainIdDetectionError, which wiped the
connected shim and set disconnected — permanently disabling auto-reconnect
until the user connected by hand, with nothing shown to explain it.

Read accounts before the chain id and bail with ConnectorNotConnectedError.
Apply the same narrowing across binance, bitget and onekey so the
UserRejectedRequestError catch wraps only requestAccounts(), matching
unisat: it is the only step a user can actually reject.

Adds the reconnect-with-no-accounts case to the shared connector spec,
asserting the throw type and that neither shim is disturbed.
@chybisov
chybisov force-pushed the fix/passive-reconnect-connectors branch from 96a65d0 to d9359c6 Compare July 27, 2026 12:08
@chybisov
chybisov merged commit f427391 into main Jul 27, 2026
6 checks passed
@chybisov
chybisov deleted the fix/passive-reconnect-connectors branch July 27, 2026 12:12
@github-actions github-actions Bot mentioned this pull request Jul 27, 2026
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.

1 participant