Background
We recently shipped staged "Connecting…" progress for the Unisat BTC wallet in PR #1792. Before that change, the wallet-connect modal showed a single opaque "Connecting Unisat" spinner the whole way through. After it, the modal walks the user through each phase ("Connecting Unisat" / "Checking version" / "Switching to Signet" / etc.) and surfaces an actionable description below the title when the wallet is waiting on the user (e.g. "Approve the connection request in your Unisat extension").
The PR added the plumbing once and used it only for Unisat. Every other BTC wallet still shows the old single-string Connecting <name> message. This issue is the follow-up to extend the same UX to OKX, OneKey, Ledger (v1), Ledger (v2), and Keystone. AppKit is intentionally not in scope.
What's already in place (do not redo)
PR #1792 already shipped:
ProgressReporter type in packages/babylon-wallet-connector/src/core/types.ts — (message?: string, description?: string) => void.
IProvider.connectWallet accepts an optional onProgress: ProgressReporter. Providers that don't pass it simply ignore it.
WalletConnector.connect creates a reporter wired to its connecting event and threads it into Wallet.connect → provider.connectWallet(onProgress).
connecting event signature is (message?, description?). useWalletConnectors forwards both args into displayLoader.
LoaderScreen renders a title and an optional secondary description text below it.
The work in this issue is only inside each provider's connectWallet method. No infrastructure changes are needed.
What needs to happen for each wallet
The pattern is identical for all five providers:
- Import
ProgressReporter from @/core/types.
- Change
connectWallet = async (): Promise<void> => {...} to connectWallet = async (onProgress?: ProgressReporter): Promise<void> => {...}.
- Call
onProgress?.(title, description?) at each meaningful phase boundary inside that function (and inside any helper it calls, by threading onProgress through as an argument).
- Keep the string literals inline in the provider — that's the existing convention in this package and is what Unisat does. Do not move these to the vault's
copy.ts.
Per-wallet stage maps and gotchas below.
1. OKX — packages/babylon-wallet-connector/src/core/wallets/btc/okx/provider.ts
OKX has only a single interactive RPC (this.provider.connect()) which returns { address, compressedPublicKey } in one call, so there is essentially one stage to surface — the approval prompt itself. There is no separate version check, no switchChain flow (OKX picks the right provider object at construction time via wallet[providerName]).
| # |
Trigger |
Title |
Description |
| 1 |
Before this.provider.connect() (file lines 48–51) |
Connecting OKX |
Approve the connection request in your OKX extension |
Notes:
- The default
Connecting OKX is already emitted by WalletConnector.ts before the provider runs, but it has no description. Calling onProgress?.(...) at the start of connectWallet upgrades it to title + description.
- Optional follow-up (not required for this issue): wrap
this.provider.connect() in withTimeout like Unisat does, with a "did not respond" recovery message. Today OKX can hang forever if the extension is wedged.
2. OneKey — packages/babylon-wallet-connector/src/core/wallets/btc/onekey/provider.ts
OneKey's connect runs three steps: this.provider.connectWallet() (interactive), then this.provider.getAddress(), then this.provider.getPublicKeyHex(). The single-address-mode error is already caught and surfaced as WALLET_CONFIG_REQUIRED — leave that logic untouched.
| # |
Trigger |
Title |
Description |
| 1 |
Before this.provider.connectWallet() (file lines 33–35) |
Connecting OneKey |
Approve the connection request in your OneKey extension |
| 2 |
After connectWallet resolves, before getAddress() (file line 61) |
Finalizing connection |
(none) |
Notes:
- OneKey has a
getNetwork/mapOneKeyNetwork capability but currently does not align the wallet with config.network during connect (unlike Unisat's ensureExpectedChain). Adding such a check is a separate improvement and out of scope for this issue — file it as its own follow-up if desired.
- Optional follow-up: same
withTimeout wrap as OKX.
3. Ledger v1 — packages/babylon-wallet-connector/src/core/wallets/btc/ledger/provider.ts
Ledger is a hardware device, so the user has to plug it in, unlock, open the BTC app, and confirm the address on the physical screen. This is the wallet that benefits most from stage messages because the user has multiple offline actions to take.
| # |
Trigger |
Title |
Description |
| 1 |
Before this.createAppClient() (file line 103) |
Connecting Ledger |
Plug in your Ledger, unlock it, and open the Bitcoin app |
| 2 |
After transport opens, before app.getMasterFingerprint() (file line 106) |
Reading device |
(none) |
| 3 |
Before this.getTaprootAccount(...) which calls app.getWalletAddress(..., true) (file line 117) |
Verify address on device |
Confirm the address shown on your Ledger screen |
Notes:
getWalletAddress(...) is called with showOnScreen=true, which is the moment the user must press both buttons on the Ledger to approve. That's why phase 3 needs its own description.
getWalletPolicy(...) currently calls app.getExtendedPubkey(derivationPath) a second time (line 71 is reached via line 113 after we already fetched it at line 111). That's a pre-existing minor inefficiency, not part of this issue.
4. Ledger v2 — packages/babylon-wallet-connector/src/core/wallets/btc/ledger-v2/provider.ts
Same shape as v1 plus an explicit firmware version check up front (getBbnVersion(app.transport) at file line 161). The Native SegWit / Taproot purpose decision is set in the constructor, so no extra UI stage there.
| # |
Trigger |
Title |
Description |
| 1 |
Before this.createAppClient() (file line 158) |
Connecting Ledger |
Plug in your Ledger, unlock it, and open the Bitcoin app |
| 2 |
Before getBbnVersion(app.transport) (file line 161) |
Checking Ledger firmware |
(none) |
| 3 |
After firmware check, before app.getMasterFingerprint() (file line 168) |
Reading device |
(none) |
| 4 |
Before this.getLedgerAccount(...) (file line 179) |
Verify address on device |
Confirm the address shown on your Ledger screen |
Notes:
- Phase 2 should appear briefly before the firmware version error (if firmware < v2) so the user can see why the connect failed even though they were never asked to approve anything.
5. Keystone — packages/babylon-wallet-connector/src/core/wallets/btc/keystone/provider.ts
Keystone is QR-based: the SDK pops its own scanner overlay with its own copy. While that overlay is open the wallet-connector's loader sits behind it, so stage messages matter less than for the other wallets — but we should still surface the "scan a QR" state up front and a brief "Reading account" after.
| # |
Trigger |
Title |
Description |
| 1 |
Before keystoneContainer.read([...]) (file line 55) |
Connecting Keystone |
Scan the QR code shown on your Keystone device |
| 2 |
After QR read succeeds, before dataSdk.parseAccount(...) (file line 88) |
Reading account |
(none) |
Notes:
- The Keystone SDK's own
read() call already supplies a title and description for the overlay it owns (file lines 56–68). Our onProgress messages are for the wallet-connector loader behind that overlay — they're what the user sees if they dismiss or before the overlay paints.
- The
decodedResult.status === ReadStatus.canceled and other error branches already throw typed WalletErrors — leave those alone.
Acceptance criteria
- Each of the five providers above emits at least one
onProgress(title, description) call covering the "user must act" phase (extension popup approval, plug-in-and-unlock, QR scan).
- Hardware-wallet providers (Ledger v1, v2, Keystone) additionally emit a non-description intermediate stage so the loader text changes visibly during the multi-step flow.
- No regressions:
- Existing error paths (
CONNECTION_REJECTED, WALLET_CONFIG_REQUIRED for OneKey, CONNECTION_CANCELED / QR_READ_ERROR for Keystone, firmware-too-low for Ledger v2) still surface unchanged.
- Other BTC providers not touched in this issue (Unisat, AppKit, injectable) keep their current behaviour.
pnpm --filter @babylonlabs-io/wallet-connector run build and pnpm --filter @babylonlabs-io/wallet-connector run lint are clean.
Manual test plan
For each of OKX, OneKey, Ledger (whichever version is active), Keystone, run pnpm --filter vault run dev and open Connect → BTC → :
- OKX: title flips from default
Connecting OKX to the same title with the new description "Approve the connection request in your OKX extension". Approve in extension → connected.
- OneKey (happy path): see
Connecting OneKey + description, approve, then briefly Finalizing connection, then connected.
- OneKey (single-address-mode off in wallet settings): existing error dialog still appears with the existing wording. No regression.
- Ledger v1 / v2 (happy path): loader walks through
Connecting Ledger (with plug-in instructions) → Reading device → Verify address on device (with confirm-on-screen instructions). Press both buttons on the device → connected.
- Ledger v2 with old firmware: loader briefly shows
Checking Ledger firmware, then the firmware-too-low error is surfaced via the existing error dialog.
- Keystone (happy path): before the SDK overlay paints, loader shows
Connecting Keystone with QR-scan instructions; after the scan completes, briefly Reading account; then connected.
- Keystone (canceled QR scan): existing
CONNECTION_CANCELED error path unchanged.
Reference
- Original Unisat implementation that established the pattern, type system, and loader UI: PR #1792.
- The Unisat provider is the worked example to copy from:
packages/babylon-wallet-connector/src/core/wallets/btc/unisat/provider.ts — see how onProgress is threaded into ensureExpectedChain for an example of passing the reporter into a helper.
Out of scope
- AppKit (Reown) BTC connector.
- Wrapping interactive calls in
withTimeout for OKX / OneKey / Ledger / Keystone. Worth doing separately; not required for stage messages.
- Adding a
ensureExpectedChain-style network alignment to OneKey. Separate enhancement.
- Any changes to
Wallet.ts, WalletConnector.ts, State.context.tsx, Screen.tsx, Loader/index.tsx, useWalletConnectors.tsx — these were finalised in PR #1792.
- Cancel / back UI inside the loader. The loader stays non-interactive.
Background
We recently shipped staged "Connecting…" progress for the Unisat BTC wallet in PR #1792. Before that change, the wallet-connect modal showed a single opaque "Connecting Unisat" spinner the whole way through. After it, the modal walks the user through each phase ("Connecting Unisat" / "Checking version" / "Switching to Signet" / etc.) and surfaces an actionable description below the title when the wallet is waiting on the user (e.g. "Approve the connection request in your Unisat extension").
The PR added the plumbing once and used it only for Unisat. Every other BTC wallet still shows the old single-string
Connecting <name>message. This issue is the follow-up to extend the same UX to OKX, OneKey, Ledger (v1), Ledger (v2), and Keystone. AppKit is intentionally not in scope.What's already in place (do not redo)
PR #1792 already shipped:
ProgressReportertype inpackages/babylon-wallet-connector/src/core/types.ts—(message?: string, description?: string) => void.IProvider.connectWalletaccepts an optionalonProgress: ProgressReporter. Providers that don't pass it simply ignore it.WalletConnector.connectcreates a reporter wired to itsconnectingevent and threads it intoWallet.connect→provider.connectWallet(onProgress).connectingevent signature is(message?, description?).useWalletConnectorsforwards both args intodisplayLoader.LoaderScreenrenders a title and an optional secondarydescriptiontext below it.The work in this issue is only inside each provider's
connectWalletmethod. No infrastructure changes are needed.What needs to happen for each wallet
The pattern is identical for all five providers:
ProgressReporterfrom@/core/types.connectWallet = async (): Promise<void> => {...}toconnectWallet = async (onProgress?: ProgressReporter): Promise<void> => {...}.onProgress?.(title, description?)at each meaningful phase boundary inside that function (and inside any helper it calls, by threadingonProgressthrough as an argument).copy.ts.Per-wallet stage maps and gotchas below.
1. OKX —
packages/babylon-wallet-connector/src/core/wallets/btc/okx/provider.tsOKX has only a single interactive RPC (
this.provider.connect()) which returns{ address, compressedPublicKey }in one call, so there is essentially one stage to surface — the approval prompt itself. There is no separate version check, noswitchChainflow (OKX picks the right provider object at construction time viawallet[providerName]).this.provider.connect()(file lines 48–51)Connecting OKXApprove the connection request in your OKX extensionNotes:
Connecting OKXis already emitted byWalletConnector.tsbefore the provider runs, but it has no description. CallingonProgress?.(...)at the start ofconnectWalletupgrades it to title + description.this.provider.connect()inwithTimeoutlike Unisat does, with a "did not respond" recovery message. Today OKX can hang forever if the extension is wedged.2. OneKey —
packages/babylon-wallet-connector/src/core/wallets/btc/onekey/provider.tsOneKey's connect runs three steps:
this.provider.connectWallet()(interactive), thenthis.provider.getAddress(), thenthis.provider.getPublicKeyHex(). The single-address-mode error is already caught and surfaced asWALLET_CONFIG_REQUIRED— leave that logic untouched.this.provider.connectWallet()(file lines 33–35)Connecting OneKeyApprove the connection request in your OneKey extensiongetAddress()(file line 61)Finalizing connectionNotes:
getNetwork/mapOneKeyNetworkcapability but currently does not align the wallet withconfig.networkduring connect (unlike Unisat'sensureExpectedChain). Adding such a check is a separate improvement and out of scope for this issue — file it as its own follow-up if desired.withTimeoutwrap as OKX.3. Ledger v1 —
packages/babylon-wallet-connector/src/core/wallets/btc/ledger/provider.tsLedger is a hardware device, so the user has to plug it in, unlock, open the BTC app, and confirm the address on the physical screen. This is the wallet that benefits most from stage messages because the user has multiple offline actions to take.
this.createAppClient()(file line 103)Connecting LedgerPlug in your Ledger, unlock it, and open the Bitcoin appapp.getMasterFingerprint()(file line 106)Reading devicethis.getTaprootAccount(...)which callsapp.getWalletAddress(..., true)(file line 117)Verify address on deviceConfirm the address shown on your Ledger screenNotes:
getWalletAddress(...)is called withshowOnScreen=true, which is the moment the user must press both buttons on the Ledger to approve. That's why phase 3 needs its own description.getWalletPolicy(...)currently callsapp.getExtendedPubkey(derivationPath)a second time (line 71 is reached via line 113 after we already fetched it at line 111). That's a pre-existing minor inefficiency, not part of this issue.4. Ledger v2 —
packages/babylon-wallet-connector/src/core/wallets/btc/ledger-v2/provider.tsSame shape as v1 plus an explicit firmware version check up front (
getBbnVersion(app.transport)at file line 161). The Native SegWit / Taproot purpose decision is set in the constructor, so no extra UI stage there.this.createAppClient()(file line 158)Connecting LedgerPlug in your Ledger, unlock it, and open the Bitcoin appgetBbnVersion(app.transport)(file line 161)Checking Ledger firmwareapp.getMasterFingerprint()(file line 168)Reading devicethis.getLedgerAccount(...)(file line 179)Verify address on deviceConfirm the address shown on your Ledger screenNotes:
5. Keystone —
packages/babylon-wallet-connector/src/core/wallets/btc/keystone/provider.tsKeystone is QR-based: the SDK pops its own scanner overlay with its own copy. While that overlay is open the wallet-connector's loader sits behind it, so stage messages matter less than for the other wallets — but we should still surface the "scan a QR" state up front and a brief "Reading account" after.
keystoneContainer.read([...])(file line 55)Connecting KeystoneScan the QR code shown on your Keystone devicedataSdk.parseAccount(...)(file line 88)Reading accountNotes:
read()call already supplies atitleanddescriptionfor the overlay it owns (file lines 56–68). OuronProgressmessages are for the wallet-connector loader behind that overlay — they're what the user sees if they dismiss or before the overlay paints.decodedResult.status === ReadStatus.canceledand other error branches already throw typedWalletErrors — leave those alone.Acceptance criteria
onProgress(title, description)call covering the "user must act" phase (extension popup approval, plug-in-and-unlock, QR scan).CONNECTION_REJECTED,WALLET_CONFIG_REQUIREDfor OneKey,CONNECTION_CANCELED/QR_READ_ERRORfor Keystone, firmware-too-low for Ledger v2) still surface unchanged.pnpm --filter @babylonlabs-io/wallet-connector run buildandpnpm --filter @babylonlabs-io/wallet-connector run lintare clean.Manual test plan
For each of OKX, OneKey, Ledger (whichever version is active), Keystone, run
pnpm --filter vault run devand open Connect → BTC → :Connecting OKXto the same title with the new description "Approve the connection request in your OKX extension". Approve in extension → connected.Connecting OneKey+ description, approve, then brieflyFinalizing connection, then connected.Connecting Ledger(with plug-in instructions) →Reading device→Verify address on device(with confirm-on-screen instructions). Press both buttons on the device → connected.Checking Ledger firmware, then the firmware-too-low error is surfaced via the existing error dialog.Connecting Keystonewith QR-scan instructions; after the scan completes, brieflyReading account; then connected.CONNECTION_CANCELEDerror path unchanged.Reference
packages/babylon-wallet-connector/src/core/wallets/btc/unisat/provider.ts— see howonProgressis threaded intoensureExpectedChainfor an example of passing the reporter into a helper.Out of scope
withTimeoutfor OKX / OneKey / Ledger / Keystone. Worth doing separately; not required for stage messages.ensureExpectedChain-style network alignment to OneKey. Separate enhancement.Wallet.ts,WalletConnector.ts,State.context.tsx,Screen.tsx,Loader/index.tsx,useWalletConnectors.tsx— these were finalised in PR #1792.