fix(frontend): Keplr + Ledger Nano signing stall (chain refresh, amino, recovery UX) #567
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#567
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Keplr connected to a Ledger Nano X can stall on the Keplr–Ledger signing UI during a Terra Classic swap (first report: USTR). The swap itself succeeded after the user refreshed chains in Keplr, installed/opened Cosmos then switched back to Luna Classic on the device, and retried. The dApp currently has no Ledger detection, no pre-sign Keplr chain refresh, no hardware-wallet recovery copy, and no bounded wait on the split-path sign step — so first-time Ledger users look stuck.
Not USTR-specific. Same sign/broadcast stack is used for Swap, Trade market, limits, pool, wrap, and
/ust1.User report (2026-08)
Environment inferred from report: columbus-5, Keplr extension → Ledger Nano X, first USTR swap. Tx eventually succeeded.
Current codebase
Connect and chain metadata
frontend-dapp/src/services/terraclassic/wallet.ts— Keplr extensionexperimentalSuggestChainruns once at connect. Station gets a second post-connect suggest (#127 / #208). Keplr does not.frontend-dapp/src/services/terraclassic/terraChainSuggestion.ts— Terra Classic suggestion uses bip44 coin type 330,ulunafees, chain id fromNETWORKS. Ledger Cosmos Hub is typically 118 + Cosmos app; Terra Classic needs the Terra / Luna Classic app on the device.frontend-dapp/src/services/terraclassic/stationExtensionConfig.ts—prepareStationExtensionForTerraClassicSign()re-suggests chain immediately before every Station sign.terraBroadcast.tscalls this only whenwallet.id === STATION. Keplr has no equivalent pre-sign refresh — this is the closest code-side match to the user’s “refresh was needed”.Sign / broadcast
broadcastTerraExecuteContracts→ splitsignTerraTxRaw+ RPC broadcast for Keplr extension (#359), or atomicwallet.broadcastTxfor some WC wallets.terraWalletSignTxRaw.tswalletUsesAmino():wallet.useAmino === true(cosmesKeplrExtensionctor, fed fromgetKey().isNanoLedgerin upstream). OtherwisesignDirect.walletUsesAminois not exported and has no unit tests. IfuseAminois missing on the connected object, Ledger Keplr would SIGN_MODE_DIRECT, which commonly hangs on the Keplr–Ledger UI unless the Cosmos app + blind signing are in a good state.patches/@goblinhunt+cosmes+0.0.71-ghunt.22.patch): Station always amino because “Ledger already required amino in upstream cosmes” (#208). Keplr still depends onisNanoLedger→useAmino.broadcastTx/ LCD poll are capped (30s / 90s). Split-pathsignTerraTxRawis unbounded. A Ledger hang stays on phasesigningwith button “Signing…” forever. If anyone later wraps sign in the 30s broadcast timeout, Ledger users who are still unlocking the device would get the wrong copy: “Could not broadcast the transaction. Check your connection…” (terraTxTimeout.ts).terraBroadcastUi.ts/TerraBroadcastPendingLink.tsx—signinghas no status message. Recovery copy exists only for post-signrecovering.vite-env.d.tstypesgetKey().isNanoLedgeron Station’s Keplr shim. The dApp never reads it for UX.Product / QA docs today
AGENTS_FRONTEND_SWAP_SIGNING_CONFIRMATION.md).docs/qa-onboarding.md) lists Keplr software extension only. No Ledger Nano row. Launch Keplr QA (docs/runbooks/extension-fee-guard-wallet-qa.md) is software Keplr / simulated wallet.docs/user-incident-faq.mdcovers pause / blacklist / wrap pause — not Ledger/Keplr HID stalls.Related (do not duplicate): #127 LocalTerra fees, #208 Station amino / no second
signAmino, #173 broadcast/poll timeout, #359 post-sign recovery (must not invite immediate retry after a signature exists), #429 mainnet fee guard off, #235 Station not on LocalTerra, #489 retail copy.Why a new implementation is needed
The protocol and quote path worked. The wallet transport did not complete until the user performed a Ledger app switch + Keplr chain refresh that the dApp never mentioned.
Without this work:
Constraints / guardrails
TERRA_TX_BROADCAST_TIMEOUT_MSto Ledger signing. That timeout is for RPC/transport. Ledger confirmations need a longer, separately labeled sign wait (minutes, not 30s). Wrong copy must not say “check your connection”.signDirectis the working mainnet path. Amino is required for Ledger (isNanoLedger/useAmino), matching upstream cosmes and Station’s Ledger comment in the patch. Do not re-promptsignAminoafter approval (#208).columbus-5(#429 / SEC-E08).experimentalSuggestChainfor Keplr must be best-effort (warn, don’t fail the swap) and must not spam an “add chain” modal every trade when the chain is already present (Station already does this pattern).isNanoLedgeris true). Do not change contract/indexer code.Relevant files
frontend-dapp/src/services/terraclassic/wallet.tsfrontend-dapp/src/services/terraclassic/terraChainSuggestion.tsfrontend-dapp/src/services/terraclassic/stationExtensionConfig.tsfrontend-dapp/src/services/terraclassic/terraBroadcast.tsfrontend-dapp/src/services/terraclassic/terraWalletSignTxRaw.tswalletUsesAmino— Ledger must amino; export for testsfrontend-dapp/src/utils/terraTxTimeout.ts/terraBroadcastUi.tsfrontend-dapp/src/components/ui/TerraBroadcastPendingLink.tsxsigningfrontend-dapp/src/vite-env.d.tsisNanoLedgeralready typedfrontend-dapp/patches/@goblinhunt+cosmes+*.patchisNanoLedgermore reliably — update.cosmes-patch-sha256+cosmesPatch127.test.tsdocs/frontend.mddocs/qa-onboarding.md/QA_TEMPLATE.md/docs/runbooks/extension-fee-guard-wallet-qa.mddocs/user-incident-faq.mdor a short user-help sectionskills/AGENTS_FRONTEND_STATION_SIGNING.mdor newAGENTS_FRONTEND_KEPLR_LEDGER.mdterraWalletSignTxRaw(or new) amino vs direct; broadcast UI copy;make verify-issue-567Recommended direction
getKey(chainId).isNanoLedger(and/orConnectedWallet.useAmino). Keep it on the connected wallet for the session.prepareKeplrExtensionForTerraClassicSign(name flexible):experimentalSuggestChain(getTerraChainSuggestion())before Keplr extension sign, same call sites as Station’s prepare. Failures →console.warn, continue.walletUsesAminois true for Keplr whenuseAminoor detected Nano Ledger. Unit-test both; neversignDirectfor Ledger.phase === 'signing'and Ledger (or after ~10–15s for any Keplr extension): short hint — open Terra Classic (LUNA) on the device, not Cosmos; approve in Keplr; if it stays blank, refresh Terra Classic in Keplr and retry.make verify-issue-567: invariants indocs/frontend.md, QA matrix row, FAQ recovery, playbook.Acceptance criteria
signDirectunless wallet says amino). No new modal spam.isNanoLedger): split path uses amino; neversignDirect.signing, Ledger (or delayed Keplr) users see recovery hint (LUNA app, not Cosmos; refresh chain if stuck). Button may staySigning….#429mainnet fee guard stays off;#208no secondsignAmino.make verify-issue-567.Test plan (functional)
useAmino/isNanoLedgertrue →signAmino+preferNoSetFee; false →signDirect. Station/Cosmostation unchanged (always amino).phase=signingshows hint; software Keplr does not show Ledger-only text at t=0 (delayed generic “approve in Keplr” OK).recoveringcopy unchanged (#359).signAminopast bound → Ledger recovery message;isPendingclears; noTERRA_TX_BROADCAST_TIMEOUT_MESSAGE. HungsignAminothat later resolves after UI timeout must not double-broadcast (ignore late signature or lock).transactions.test.ts,terraBroadcastRecovery.test.ts).Transaction rejected by user; form re-enabled; no broadcast.make test-frontend;make verify-issue-429; Station signing tests; Swap pre-sign panel (#409) still present.Test plan (attack / abuse)
getTerraChainSuggestion()(our RPC/LCD/coin type 330). Do not pass wallet-supplied chain JSON.Fee+preferNoSetFee.javascript:/ unexpected URLs in hintVerification
make verify-issue-567(new): unit tests for amino/Ledger, pre-sign suggest hook, signing hint, sign-stall copy ≠ broadcast timeout; doc markers indocs/frontend.md+ QA matrix + playbook.make test-frontendgreen;make verify-issue-429green.Owner type
frontend
Priority
medium — funds were not lost; first-attempt Ledger UX is a production support trap.
changed the description
mentioned in merge request !1091
mentioned in commit
adc91bb474mentioned in commit
d89a8c7a61Post-merge note (!1091 on main). K567 amino Ledger path, pre-sign suggest, 4-minute sign wait, and stall copy (not the 30s broadcast timeout) are on
main.Remaining:
make verify-issue-567.mentioned in issue #573
marked as related to #573