UX: Rate-limit blocked transfers stuck silently on step 4/4 with no user-facing reason #127
Labels
No labels
agent:implement
agent:ready
backend
bug
cannot-reproduce
confirmed
desktop
docs
documentation
duplicate
enhancement
feature
frontend
good first issue
help wanted
high-risk
in-review
invalid
mobile
needs-triage
P0-critical
P1-high
P2-medium
P3-low
qa
QA
question
ready
report
responsive
security
security-escalate
smart-contract
solana
tablet
test-pass
ux
wallet-issue
wallet:keplr
wallet:metamask
wallet:station
wallet:walletconnect
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-bridge-monorepo#127
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
When a bridge transfer hits a destination chain rate limit, the TransferStatus page stays on step 4/4 ACTIVE indefinitely with no user-facing indication of why it is stuck. Verify page shows the transfer as a successful verify (correct on-chain), but the UI gives the user no reason for the apparent freeze.
Repro (today, 2026-05-01)
Transfers stuck >5 min past cancel window with no error surfaced to UI:
0x8f3cb99bd1ffcf0e8738f6ce769a32fb4b8c0eeb71d71b85ffb4bb6073a789950x45ded3ea2eb82ae066827c38305f2d331547f785a4bac7fb4b54d55d0b0fe4e2Both stayed on step 4/4 "Delivering tokens to recipient" with
CANCEL WINDOW ACTIVE / Executing... Operator usually completes within ~30 secondsindefinitely. Per @PlasticDigits Telegram: rate-limit blocked the withdrawal correctly, behavior on-chain is correct, but the frontend never surfaces the rate-limit reason to the user.BSC → MegaETH testa same session completed cleanly (xchain
0x7e4317bac1b3f6e7a1a579bbff63f0961afb10b22a72c9d9ecc90807edb6df38) — only Terra and opBNB sources hit the limit.Expected
When a transfer is blocked by a rate limit (or any operator-side condition), the TransferStatus page should surface the reason to the user instead of staying silently stuck on step 4/4. Suggested copy: a banner under step 4 like
BLOCKED: rate limit on destination — try again in <window>, or similar.Severity
P2 medium UX — not on-chain incorrect, but user-facing behavior is opaque and looks like a hung transfer.
cc @PlasticDigits
mentioned in issue #129
mentioned in commit
22878c45e6Implemented (GL-127) — merged to
main@eaa3d0aSummary: Transfer Status now classifies EVM destination rate-limit stalls (MegaETH / any EVM bridge using
TokenRegistry.getWithdrawRateLimitWindow) the same way we already did for Terra, and shows a 1-second-updating “Resets in …” countdown aligned to the RPC snapshot (fetchedAtWallMs).Code:
useEvmExecutionRateLimitStatus→useTokenDetails+computeEvmExecutionRateLimitStatus;useWithdrawRateLimitCountdown; EVMgetPendingWithdrawmapping now includesdestDecimalsfor correct payout normalization (matchesBridge._normalizeDecimals). Terra queries sharenormalizeBridgeAmountToDestDecimals; temporary blocks attach optionalfetchedAtWallMsfor the same countdown alignment.Docs:
docs/FRONTEND_BRIDGE_INVARIANTS.mdINV-UX2;skills/agent-frontend-bridge-chains.mdcrosslink + invariant bullet.@brouie — please verify on a rate-limit repro (e.g. Terra/opBNB → MegaETH) when you have a moment. Issue left open per request.
Verification checklist
/transfer/<xchainHashId>for an approved, non-executed transfer to an EVM dest when the destination 24h withdraw window is exhausted: amber Blocked: destination rate limit + Resets in countdown ticks every second until window end.fetchedAtWallMsis present.useTokenDetailspath).Tests run:
packages/frontendnpm run test:unit,npm run build; pre-commit hooks (tsc, eslint) on merge.mentioned in issue #130
mentioned in issue #123
Source-level pre-walkthrough on
eaa3d0a. Implementation looks solid end-to-end.Files in place
services/evmExecutionRateLimit.ts— pure classifier, returns unifiedTerraRateLimitStatusshape (so EVM and Terra paths share the same status type per your commit notes).services/evmExecutionRateLimit.test.ts— 6 tests covering every branch (unknown× 2,permanently-blocked,temporarily-blocked,ok, decimal-norm parity).hooks/useEvmExecutionRateLimitStatus.ts— composesuseTokenDetails+ classifier, gates ondest.approved && !dest.executed.hooks/useWithdrawRateLimitCountdown.ts— 1HzsetIntervalwith wall-time anchored countdown; matchesSourceChainSelectorpattern.utils/bridgeAmountDecimals.ts—normalizeBridgeAmountToDestDecimalsmirrorsBridge._normalizeDecimals1:1 across all 3 branches (equal / down / up).pages/TransferStatusPage.tsx— wires the hook with defensive null gating (only fires whenrateLimitTemporarilyBlocked === true).Cross-module invariant verified
Decimal-normalization parity between
utils/bridgeAmountDecimals.ts(TS) andBridge.sol:823(Solidity):return amountreturn amountamount / (10 ** (src - dest))amount / pow10BigInt(srcDec - destDec)amount * (10 ** (dest - src))amount * pow10BigInt(destDec - srcDec)destDecimalspropagation end-to-end: ABI fields atwithdrawSubmit.ts:53+tokenRegistry.ts:50→getDestinationMappingreturns{ destToken, destDecimals }→ lifted into unified mapping → reachescomputeEvmExecutionRateLimitStatusviadest.destDecimalsfor payout normalization. Tested atevmBridgeQueries.test.ts:106,131andevmExecutionRateLimit.test.ts:22,79.Tests
npx vitest run evmExecutionRateLimit: 6/6 PASS in 9ms.npx vitest run bridgeAmountDecimals: 3/3 PASS in 7ms (all 3 decimal-norm branches).npx vitest run→ 638/638 across 81 files PASS in 38s. No regression on the existing Terra path.cc @PlasticDigits
mentioned in issue #128
mentioned in issue #131
Production deploy gap — sign-off blocked
Verified prod bundle on bridge.cl8y.com today: footer reads
v0.1.345 · 27a5e42. RepomainHEAD is currently at95f8fd5, which is the merge commit containing all four fixes (#127eaa3d0a, #128b6c5b5a, #130 Terra||clause drop, #13167a48e4). Prod is therefore still on the pre-fix bundle and none of the four fixes are live yet.Confirming repro on prod (proves bundle is pre-fix):
ETHtext fallback while MetaMask is on MegaETH (chain 4326). This is the clean #131-D pre-fix behavior —getChainLogoPathhas no branch for 4326, falls through togetGasSymboldefaultETH.Cannot run mainnet sign-off on the verification checklist for #127 / #128 / #130 / #131 until prod cuts to current HEAD (or whichever release commit contains the four fixes).
Will re-run the combined live walk on bridge.cl8y.com once deploy is confirmed:
Plus the 4 #123 blocked rows pick-up that rides on #128 / #130 fixes.
cc @PlasticDigits
@Brouie bridge.cl8y.com frontend is now deployed at latest commit.
queued in the bundled bridge live-walk along with #128 — see https://gitlab.com/PlasticDigits/cl8y-bridge-monorepo/-/work_items/128#note_3322750554 for timing.
mentioned in issue #132