fix: bound the All Networks header hold and split the 6.5.2 home refresh hotfix OK-61576 - #13124
Conversation
…shot before asset analytics OK-61576
…sisting as network totals OK-61576
…annot pin a stale total OK-61576
Stacked on fix/wallet-home-header-hold-v6.5.2 (#13124). This commit carries only the IAssetSnapshotMeta freshness system so it can soak separately: - shared: assetSnapshotFreshness (localSeq primary, HTTP Date tie-break, watermark self-heal), IAssetSnapshotMeta types, resp.assetSnapshotMeta - bg: ServiceToken mints one marker per fetch; ServiceAccountProfile admits active-account-value writes under a mutex; SimpleDb accountValue / localTokens admit same-or-newer supplied keys, strictly newer for omitted keys, tolerate pre-migration legacy buckets, skip no-op writes - main: accountOverview.updateAccountWorth admission (equal markers admitted for full replacements, retained currency when every value is rejected, createAtNetworkWorth recomputed from absolute values, reset flag), buildMergedAllNetworkSnapshot / TokenListBlock / HomeOverviewContainer / TokenSelector thread the markers through to persistence
|
@codex review |
|
@codex security review |
|
@cursoragent review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
PR #13124 代码审查报告审查基准是 审查概要
评分
PR 评论分析
发现的问题[🟡 中] [🟠 Medium]
|
| 优先级 | 置信度 | 文件 | 类型 | 描述 | Auto-fix |
|---|---|---|---|---|---|
| 🟡 中 | 🟠 Medium | accountOverview/actions.ts:69 + HomeOverviewContainer.tsx:737 |
运行时 | updateAll 被 merge 清空,grace 可能提前结束或闪回 |
— |
| 🟢 低 | 🔵 High | useAllNetwork.ts:1188 |
运行时 | debounce 预约未覆盖 DeFi/NFT | — |
| 🟢 低 | 🔵 High | DeFiListBlock.cacheOnlyGate.test.ts |
规范 | 源码字符串测试 | — |
测试建议
- Tokens 冷启动、不进 DeFi:确认
all-network-hook-gate出现shouldAlwaysFetch:true(reason:defi),随后有all-network-hook-run-start;头图在 token commit 后约 5s 内离开旧 confirmed,和列表收敛。 - 1s 内双击刷新:token 侧应只有一次 fan-out + 一次 queued must-run,最后有
all-network-authoritative-commit。 - 刷新后后台 >30s 再回前台:头图和列表同一快照。
- 慢设备 / HW:cache hydrate 后故意拖住第一条 settle >5s,看头图会不会先放出再闪回旧值。
- 大额 DeFi 仓位:grace 到期而 DeFi 仍未 ready 时,头图会按 tokens + 0 DeFi 显示(
resolvedBalanceString在!isCurrentAccountDeFiReady时把 DeFi 当 0),然后再跳上来。这是刻意取舍,不要当回归失败,但 QA 要知情。 - TokenSelector 过滤模式:打开带 filter 的选择器后回到 Home,网络总值不应被子集覆盖。
- Others 账户:
createAtNetworkWorth标量在 merge 中途变脏时,SimpleDB 应只写 compound-key 上的绝对值。
没有达到 inline comment 门槛的 finding(需要 🔴,或 🟡 且 🔵 High),所以不往 PR 上挂行内评论。若要我针对 grace/updateAll 闪回补一刀修复,直接说即可。
|
Security review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e76a06155c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- Do not arm the DeFi grace from a stale token commit while a DeFi run is in flight: a warm refresh never resets `updateAll`, but the DeFi run resets readiness at start and writes it back at finish, so the grace could drop DeFi from the header after 5 s while it was only reloading. Grace expiry is now sticky until DeFi reports for the owner. - Treat a fan-out whose every per-network request failed as a failed run: `continueOnError` resolves it with an empty result instead of throwing, which overwrote the retained last-good snapshot and left the consumer without an authoritative commit. Distinguish it from "no accounts" by the issued request count and restore the retained snapshot instead.
…s OK-61576 After the DeFi grace releases the All Networks hold, the live total zeroed the DeFi part whenever readiness was unset, even though the overview atom still held this owner's last DeFi total (a warm refresh never clears it). The released total now includes that value when the atom is stamped with the current owner, and falls back to zero only when nothing is known.
* fix: recover Perps TWAP history and stalled order book subscriptions (OK-60985, OK-61108) (#12999) * fix: handle unknown Hyperliquid TWAP history statuses (OK-60985) The SDK status union omits `waitingForTrigger` and `stopped`, which the live API returns for trigger TWAPs. The exhaustive status map resolved to undefined, formatMessage threw on the missing id, and the error bubbled to the root boundary, blanking the whole Perps page on the TWAP history tab. Widen the status union in the shared SDK type re-export so exhaustive maps fail to compile instead of throwing at render time, and fall back to raw text for any status that has no translation key yet. * fix: stop a dropped Hyperliquid subscribe ack stalling the order book (OK-61108) A subscribe frame whose server ack never arrives blocked the reconcile for the SDK's default 10s timeout, so the order book stayed empty for ~11.5s after the app resumed and the user re-entered Perps on an uncached coin. The stall only cleared when something re-triggered updateSubscriptions, which made it look like the data loaded on swipe. Cap the WebSocket request timeout at 5s. Keep it at or above reconnect.connectionTimeout so frames rews buffers during a reconnect can still flush before the abort fires. Arm the critical subscription health check before awaiting _executeSubscriptionChanges instead of after it, so a stalled reconcile still gets a watchdog. It stays below the stale-critical branch, which bumps _subscriptionLifecycleVersion and would otherwise self-invalidate a timer that captured the version before the bump. * fix: localize TWAP history statuses OK-60985 (#13003) * fix: stop the subscription watchdog tearing down healthy subscriptions The critical health check is armed before _executeSubscriptionChanges awaits, so it can fire while the reconcile that armed it is still pending. Its rebuild queues every destroy behind the stalled mutation on the same per-key queue, so the healthy subscriptions are torn down while the recovery blocks on the very stall it was meant to interrupt. Skip the rebuild while a reconcile is in flight and re-arm instead, and re-arm in a finally so a _forceReconnectTransport reached inside the reconcile does not leave the check disarmed. * fix: close the watchdog race against a reconcile started mid-check The in-flight guard only ran before buildRequiredSubscriptionsMap awaited, so a reconcile starting during that await still reached the rebuild: a plain reconcile does not bump _subscriptionLifecycleVersion, so the post-await version check cannot catch it, and its subscribes have not landed yet, which biases the critical-type checks toward reporting them missing. Re-check the flag once the map resolves. Everything after it is synchronous, so that is the last point a concurrent reconcile can slip in before the cleanup tears its subscriptions down. --------- Co-authored-by: Kahn <0xkahnchan@gmail.com> Co-authored-by: Leon <lixiao.dev@gmail.com> * feat: register the Entropy hip-3 perps dex (OK-61073, OK-60689) (#13029) * feat: register the Entropy hip-3 perps dex (OK-61073) Entropy is hyperliquid perp dex index 10, prefix `io`. Registering it in SUB_DEX_LIST is enough for the market list, token selector, favorites, universal search routing and deep links, which all read the registry. The `io` prefix is the first one that shadows a real main-DEX symbol: findDexPrefix matches bare prefixes so legacy separator-free links keep working, which turns `?token=IOTA` into a guess of `io:TA`. The universe check already rejects that guess, but when the cache is incomplete -- a cold-start deep link, exactly when this runs -- the unproven guess was kept and the page silently fell back to the previous coin. Only `xyz` ever shipped separator-free links, from before encodeCoinForUrl kept the separator, so its guess still wins without a universe. Prefixes registered since fall back to the literal token instead. `cash` (dex 7, shadows CASHCAT) would have hit the same trap. The URL codec moves to a .utils sibling so it is testable without pulling in backgroundApiProxy, matching PerpsGlobalEffects.utils. * fix: pick the perps url fallback by main-dex shadowing, not link history The fallback answers which reading could name a real market when the universe cannot settle it, but it was keyed on whether a prefix ever shipped separator-free links. Those criteria agree on xyz and io and disagree on para: no main-DEX symbol starts with PARA, so PARAUNITREE is no market at all while para:UNITREE resolves. A hand-written link went from always right to always wrong, in exactly the window this PR widens -- the fourth registry slot rejects every persisted cache, so the first cold start after the hot update runs with an incomplete universe. Key it on main-dex shadowing instead: only a prefix a real symbol starts with has a literal reading worth keeping. Narrow the prefix type so the list cannot drift from SUB_DEX_LIST. * feat: bump perps asset type version to 3 for the io dex Gate a newly registered dex behind the version so the server can keep it out of responses to clients that predate it. Search drops an unregistered prefix on its own, but that guard does not cover every surface, so the version is what actually keeps a new dex away from older builds. * fix: show the dex badge on market watchlist perps rows The watchlist renders perps rows through its own branch in the market columns, which carried the leverage badge but never the dex one, so an xyz or para market was indistinguishable from a main-DEX one there. Every other perps surface shows it. TokenIdentityItem takes a perpsDexLabel prop for this, but perps rows never reach it -- the branch above returns first -- and no caller ever passed it, so the prop reads as wired while rendering nothing. Derive the label from record.perpsCoin, which already holds the full prefixed coin. Native delegates to the mobile columns, so both platforms are covered. * fix: localize Entropy perps dex description OK-61073 (#13034) * fix: show the dex badge on mobile market watchlist rows Narrow web and native render the watchlist through MobileMarketWatchlistFlatList -> TokenListItem -> TokenIdentityItem, not through the market columns, so the badge added to useColumnsMobile never reached them. TokenListItem already forwards maxLeverage and perpsSubtitle, which is why those rows show the leverage badge and the company name but no dex tag. It just never passed perpsDexLabel, so TokenIdentityItem rendered PerpDexBadge with an undefined label and it bailed out. Derive the label from item.perpsCoin the way the columns do. Spot rows carry no perpsCoin and keep rendering nothing. * fix: send assetTypeVersion on the market and perps config requests The backend gates a newly registered dex behind `assetTypeVersion`, and a request that omits it is served the dataset from before that dex existed. Only universal search ever sent it, so search was the one surface that could see `io` while the market list, the watchlist, the token selector and the perps config all silently got the pre-`io` payload. Verified against the test server: `token-list?category=all` returns 302 tokens with no `io:` row, the same call with `assetTypeVersion=3` returns 305 including io:ANTH, io:SNDK and io:NBIS; `perp-config` returns an empty pre-ipo tab, and with the parameter it carries the configured entry. Move PERPS_ASSET_TYPE_VERSION next to SUB_DEX_LIST so registering a dex and bumping the version are read together. Keeping it private to the search service is what let two callers miss it. * fix: use FrontendMarket tif for hyperliquid market close * fix: align hyperliquid market orders to FrontendMarket tif * fix: stabilize chart limit order button width and show order value * refactor: restore original side stats guards in chart limit order --------- Co-authored-by: Kahn <0xkahnchan@gmail.com> Co-authored-by: Leon <lixiao.dev@gmail.com> * fix: refresh DeFi network config cache OK-61713 (#13126) * fix: DeFi share links, banner looping, risk gate coverage and in-app … (#13135) * fix: DeFi share links, banner looping, risk gate coverage and in-app tip links (OK-61675, OK-61479, OK-61516, OK-61515, OK-61348, OK-59196, OK-61325) - earn share links: derive the :network slug from the preset network shortcode instead of a hardcoded table, so Katana and friends stop generating /earn/unknown/... that the detail page rejects; the old ethereum/solana/aptos/cosmos spellings stay valid on parse - earn home banner: add an opt-in `infinite` mode to Carousel so the banner wraps around and never sits at a content edge, where the platform pager hands the gesture to its parent; exclude the banner area from the header's tab-switch pan, which sat on top of it; drop the mid-gesture scrollEnabled lock on the outer pager, which never covered that pan anyway - risk disclaimer: extend the one-time gate from earn deposit to every remaining trade entry (earn withdraw/claim and their approvals, all borrow actions and the borrow approve step, DeFi Portfolio one-click actions), and return a boolean from the trade hooks so a declined disclaimer keeps the typed amount and releases the submit guards instead of sticking on loading - risk notice / protocol tips dialogs now close before a link opens, instead of staying stacked behind the page - add the <urlInApp> rich-text tag: opens tip links in the in-app webview with the dapp bridge on, re-checking every top-frame navigation and showing the live host; <url> keeps going to the system browser. Server-side gating ships separately in server-service-earn, keyed on jsbundle version because the app version header does not move for a hot update Signed-off-by: ezailWang <jelly@onekey.so> * fix: Earn banner gesture handling and protocol tips overflow Carousel (Earn home banner): - Fix the white screen when a finger lands during an auto-scroll. The infinite-loop clone swap called setPageWithoutAnimation while the pager was still driven by the gesture, which UIPageViewController asserts on and ViewPager2 rejects. The jump is now held until onPageScrollStateChanged reports idle, and an autoplay beat is skipped instead of turning the page out from under a finger. - Stop a swipe from opening the card it ends on. The native pager claims the horizontal pan, so a Pressable inside a page never receives the move that would cancel it and sees only down and up. The carousel exposes useCarouselPressSuppressor and EarnHomeBanner consults it before navigating. - Resume autoplay after navigating away and back. The timer was paused by onPressIn and only restarted by onPressOut, which never arrives once the screen is leaving, and the IntersectionObserver fallback is web-only. Autoplay now follows useIsFocused, matching composite/Banner. - Keep the native-only onPageScrollStateChanged prop off the DOM node in the web pager shim. Protocol tips: - <urlInApp> links open a Discovery tab instead of the in-app WebView modal, falling back to the system browser on web and the extension, where Discovery does not exist. - Long tips scroll inside the dialog instead of overflowing the viewport on phone and desktop; drag-to-close is disabled there so the sheet stops competing with the inner scroll for the same vertical gesture. Signed-off-by: ezailWang <jelly@onekey.so> * fix: lint Signed-off-by: ezailWang <jelly@onekey.so> * fix: review p1 Signed-off-by: ezailWang <jelly@onekey.so> --------- Signed-off-by: ezailWang <jelly@onekey.so> * fix: Perps close-position price readiness, cold-start banner shift, favorites order sync and TWAP tab shadow flash(OK-61259 OK-61504 OK-61486 OK-61520) (#13128) * fix: guard rapid Perps close-position taps (OK-61259) handleClosePosition opened a new dialog on every tap, so fast taps stacked several dialogs each holding its own position snapshot, and the confirm button's disabled state landed one render too late to stop a same-frame double tap. Only one close dialog is kept alive at a time and the submit is guarded by a ref. * fix: prevent iOS Perps layout shift on cold start (#13097) (OK-61504) (cherry picked from commit 3c945b8) * fix: follow the favorites bar drag order in the footer ticker and token selector (OK-61486) usePerpsFavorites returned membership order, so the footer ticker never saw the persisted drag sequence, and the token selector applied the default volume sort on top of it. Favorites now flow through the persisted sequence everywhere, and the favorites tab only sorts after an explicit header click on that tab, matching how the category tabs already behave. * fix: stop the TWAP sub-tab switch flashing the fixed-column shadow (OK-61520) Each TWAP sub-tab mounts its own table seeded with the shadow visible, and the overflow measurement ran after paint, so a full-width table flashed the shadow for one frame on every switch. Measuring in a layout effect settles the shadow before the browser paints. * fix: keep the close-position guard armed through the dialog exit animation (OK-61259) onClose() does not await the 300ms exit animation, so releasing the guard in finally re-enabled Confirm while the dialog was still mounted; a tap in that window would send a second reduce-only order. The guard now resets only on early return or failure and otherwise dies with the dialog. * refactor: reuse the favorites sequence helper in the favorites bar (OK-61486) * fix: require a fresh close-position price (OK-61259) iOS cold start: positions, order book and the trading buttons restore from the main-runtime cold-start cache, but perpsAllMidsAtom has no cache and only fills once the background WebSocket delivers the first allMids a few seconds later. The close dialog turned the missing price into '0', Boolean('0') enabled Confirm, and the submit path rejected '0' with "Unable to get current market price" without sending an order. main runtime: a missing, zero or non-finite mid is now undefined, so Confirm stays disabled with a spinner until the price arrives. bg runtime: the submit resolves the price from the background allMids cache when it is under 5s old, otherwise from a per-dex REST allMids snapshot; the REST result is not written to the shared cache because it covers one dex only. Evidence: log of OneKey 6.6.0 build 2026082600 bundle 20484813 on iOS 26.6, first allMids reached bg 4.2s after launch, Confirm tapped 2s before that, no ordersClose RPC followed. * fix: keep the limit close price editable and share the background price gate (OK-61259) Turning a missing mid into undefined made the limit PriceInput's disabled={!midPrice} bite for the first time, locking the input and hiding the Mid shortcut during the cold-start window; the input now stays editable and only the Mid action dims. The market Confirm gate also only watched the main-runtime atom while the submit already resolved prices in the background, so the dialog now polls the same background source every 2s; when that source has no price either, the spinner stops and the existing toast tells the user, leaving the limit path as the exit. * fix: resolve Close All prices in the background and fail instead of dropping positions (OK-61259) closeAllPositions read mids from the main-runtime atom only and silently filtered out positions without one, so during the cold-start window it could close a subset while the inner ordersClose toast reported success. Missing mids now go through getMarketOrderReferencePrice, and any position still without a price aborts the whole action with a visible error. * fix: keep the fixed-column shadow transition off until the first measured paint (OK-61520) Reading scrollWidth in the layout effect forces a style flush with the seeded initialVisible shadow, so the CSS transition animated the seeded to measured change even though the seeded frame itself never painted. The transition now stays 'none' until the measured state has been painted (double rAF), then turns on for real scroll-driven changes. * fix: price every Close All position through the background owner (OK-61259) closeAllPositions took the main-runtime atom's mid whenever one existed and only asked the background for missing coins. That atom carries no age marker, so a stalled ALL_MIDS stream could bound the whole batch around an arbitrarily stale midpoint. Every coin now resolves through getMarketOrderReferencePrice, the same freshness-checked owner the single close submit uses; the all-or-nothing abort stays. * chore: mark the close-price error literal for i18n follow-up (OK-61259) --------- Co-authored-by: huhuanming <huanming@onekey.so> * feat: withdraw Perps USDC over Hyperliquid's CCTP rail(OK-61320) (#13119) * feat: withdraw Perps USDC over Hyperliquid's CCTP rail (OK-61320) Hyperliquid moved USDC to Circle CCTP and marked the legacy Arbitrum bridge deprecated. Follow the rail it serves from info/usdcRouting, which drops the withdrawal fee from $1 to $0.20 and leaves a switch back to the bridge without another release. A rail already confirmed live wins over the fallback, so one failed lookup cannot quietly downgrade a user to the deprecated bridge. `sendToEvmWithData` needs an explicit source balance where `withdraw3` let Hyperliquid pick one, so mirror perpsComputedAccountValueAtom and source from spot for unified and portfolio-margin accounts. CCTP withdrawals reach the ledger as a `send` to the HyperEVM system address rather than a `withdraw`, so teach net deposits and the account history row about that shape. Without it the balance drop reads as a trading loss and the row renders as money coming in. Net deposits counts both directions across that address so a Core/HyperEVM round trip nets to zero. * feat: add destination selection for perps USDC withdrawals (#13125) * feat: support multi-chain perps USDC withdrawals * fix: keep perps withdrawal destinations selectable * fix: drop the unreachable inbound leg from perps net deposits EVM -> Core credits arrive as a `spotTransfer` from the HyperEVM system address, never as a `send` from it, so the inbound arm never matched. Counting them properly would double up against the `accountClassTransfer` branch, which already treats spot as outside this reducer's scope, so the outbound leg stands alone and the comment now says so instead of claiming a round trip nets to zero. * fix: make perps net deposits match its own definition The stat reads "total deposits minus total withdrawals", but it also counted `accountClassTransfer`, which only moves USDC between the perp and spot balances of one account. That was already wrong by its own definition, and it became a double subtraction once a withdrawal could be sourced from spot: once on the way to spot, again on the way out to HyperEVM. Also stop treating an unreadable `usdcRouting` response as a genuine switch to the legacy bridge. `requestLoggedHyperLiquidTransport` reports `{status:'err'}` bodies but still returns them, so one malformed answer used to be cached as `bridge` for five minutes and overwrite the last confirmed rail, defeating the guard that exists to stop exactly that. * fix: refresh the perps withdrawal rail while the form is open A rail that flips mid-session made submission fail with "route changed. Review the updated fee and try again", but the modal resolved the rail once per open, so the retry it asks for could never succeed until the user closed and reopened the form. It now refreshes on the same tick as the fee quote, which the background cache already serves for free most of the time. * fix: explain why a withdrawal inside the gas reserve is rejected Withdrawals validate against the balance minus the Core -> EVM gas reserve, but the insufficient-balance check still compared against the untrimmed balance, and that untrimmed number is what the form displays as available. Typing it left the submit button disabled with no message and no way out. Both now use the same ceiling, so the amount inside the reserve reports insufficient balance. * fix: count HyperEVM credits back into perps net deposits Dropping `accountClassTransfer` committed this stat to whole-account scope and removed the reason the inbound leg was left out. Without it a Core/HyperEVM round trip subtracts on the way out and never adds on the way back, so a user who withdraws to HyperEVM and moves the funds back sees the stat drift permanently negative — and HyperEVM is now one of the offered destinations. The credits arrive as a `spotTransfer` sent by the system address. * test: cover perps net deposits accounting The reducer moved three times under review and had no test, on a stat users read as a money figure. Extracted it to portfolioStats as a pure function and covered the cases that changed: a CCTP withdrawal counting as an outflow whichever balance it is sourced from, a Core/HyperEVM round trip netting to zero, and transfers between an account's own balances staying out of it. * fix: stop the perps withdrawal fee flickering on destination change Switching destinations invalidated the quote by key, so the row rendered a placeholder between the old and new value — visible even between two destinations charging the same amount. Each destination's fee is known before the quote returns, so the row now previews it and only the value itself changes. The periodic refresh also rebuilt its quote object every tick; it now keeps the previous one when the numbers are unchanged. Submission still binds to the confirmed quote, not the preview. * chore: tighten the comments added for the CCTP withdrawal work Several ran to four or six lines restating what the code does. Kept the reason each block exists and dropped the narration. * fix: keep the perps withdraw button steady when the destination changes The button was gated on the confirmed fee quote, which is invalidated by key the moment a destination changes, so it greyed out for the frame before the new quote landed. It now gates on the same preview the fee row uses, and the wait for the confirmed quote moved into the submit handler, which still binds submission to a server-confirmed fee. * fix: stop the CCTP fee estimate marker blinking on destination change Only CCTP destinations read their fee from chain, so only they could hand the row a confirmed quote that differs from the preview: same number, but flagged as an estimate, which added and removed a prefix and read as a flicker. HyperEVM never showed it because its fee is local. The marker told the user nothing the number did not — a failed read falls back to the same value, and submission re-reads it and refuses on drift — so the row now just shows the amount. * fix: never submit a perps withdrawal against an unseen fee Gating the button on the fee preview let a tap land inside the window where the destination had changed but its quote had not, and the submit handler then fetched the live fee and signed with it. The CCTP fee comes out of the principal, so the user could be charged a number the row never showed. The button waits for the confirmed quote again, and the preview is marked as the estimate it is. * fix: quote every withdrawal destination up front The row and the submit button both waited on a per-destination quote that only started loading once the destination changed, so every switch spent a frame without one: the fee fell back to the estimate and the button greyed out. The quotes are now kept per destination and requested for all of them when the form opens, so a switch lands on one that is already confirmed. The rail is served server-side, so this runs once per open. * fix: replace the withdrawal fee placeholder with a loading state Quoting every destination up front already closed the gap a destination switch used to leave, so the estimate preview only survived on form open, where it added a second transition: placeholder, then estimate, then the confirmed number. The estimate marker appearing and vanishing is what read as a flicker. The row now shows a skeleton until the quote lands, matching the withdrawable row above it, and quotes outlive the modal so reopening the form does not blank it again while it refetches. --------- Co-authored-by: Kahn <0xkahnchan@gmail.com> * fix: backport Receive token selector search optimizations to v6.5.2 (OK-61630 OK-61484 OK-61367 OK-61365) (#13112) * feat: cross-network token search in Receive selector (OK-58557) (#12743) * feat: cross-network token search in Receive selector (OK-34006) Under a single-network scope the Receive token selector could only search the current network, so users had no way to receive a token on another chain without going back to the home page and switching to All Networks. Search now queries the backend across all networks and groups the results into a current-network section and an "Other networks" section. Combined queries ("usdt trx") first strip an exact network keyword and scope the request to that network, because the backend matches the keyword string as a whole. Selecting an other-network row resolves (or creates) the target account and goes straight to its receive address page; the home network scope is never changed. Opt-in via route params, so Send, Exchange and Swap are unaffected. Also: - searchTokens $key now carries networkId. Native tokens across chains share uniqueKey 'native' and an empty address, so the previous key collided and $key-keyed maps collapsed them into one row. - selector searches that return nothing now show a neutral "No result" instead of the browse-state "You don't hold any crypto" copy, and a failed search shows a distinct error state with retry instead of masquerading as an empty result. - token rows with no balance record render nothing instead of a "-". * fix: resolve token/network alias collisions and custom-network cold start "eth base" failed to reach Base: "eth" matches Ethereum and "base" matches Base, so two networks matched and the extractor bailed, sending the combined string to the all-network endpoint where whole-string matching finds nothing. When several words look like networks, a word that also equals its own network's symbol ("eth" -> Ethereum, symbol ETH) is the token term, not the network. Drop those and keep the word that can only be a network. If that leaves zero or several the query stays genuinely ambiguous and is passed through unchanged, so "eth sol" still does not extract. Also require `network` to be resolved before enabling cross-network search — `!network?.isCustomNetwork` reads as true while it is still undefined, so a custom network could dispatch an all-network search on a cold start. The gate is now part of the search request identity as well: it flips when `network` resolves, and the two runs previously shared a context, letting the earlier response pass the staleness check and overwrite the newer one. * fix: match multi-word network names and filter incompatible Others rows Multi-word network names were unreachable. The extractor split the query into words but compared each word against the whole network.name, so "Bitcoin Cash" matched neither "bitcoin" nor "cash" and "usdt bitcoin cash" went to the all-network endpoint as one string, where whole-string matching finds nothing. Every contiguous run of words is now matched instead, longest-first so "bitcoin cash" wins over the "bitcoin" inside it. Affects many presets — BNB Chain, zkSync Era, Manta Pacific, Ethereum Classic. Others (imported / watch-only / external) accounts hold one credential on one impl, but cross-network search offered them every network the backend returned. Tapping an incompatible row could not resolve an account, then fell through to createAddress with an Others walletId and no indexedAccountId — the HD/HW batch workflow cannot supply imported or watching credentials, so the tap died in the catch with no receive address. Those results are now filtered through accountUtils.isAccountCompatibleWithNetwork before being stored. An imported EVM key still keeps every EVM network; a watch-only account restricted to one network keeps only that one. * refactor: move account-compatibility filter out of shared into kit filterTokensByAccountNetworkCompatibility needs the kit-bg-owned IDBAccount type, and shared must not depend on another OneKey package. Moved it and its tests to packages/kit/src/utils, alongside the other unit-tested kit helpers. No behavior change; the remaining cross-network utils in shared are back to importing types only. * fix: pass indexedAccountId on cross-network press account lookup fetchAllNetworkAccounts makes ServiceAllNetwork treat the request as all-network, and with no indexedAccountId it derives one FROM the accountId via buildAllNetworkIndexedAccountIdFromAccountId — a path built for the all-networks mock id (`hd-1--0000/0`), where split('/')[1] is the index. Handing it a single-chain id parses the coin type instead: `m/44'/60'/0'/0/0` becomes index 44, i.e. `hd-1--44`. Verified at runtime — the old call shape throws `record not found: IndexedAccount hd-1--44`, which the surrounding catch swallows, leaving accounts empty and every cross-network press falling through to createAddress: the exact thing the comment on that line claimed to prevent. On a wallet that does have that index it is worse, resolving a different account whose address then renders on the receive page. Passing the scope's own indexedAccountId short-circuits the derivation. Others accounts have none and must keep taking the othersWalletAccountId branch, which supplying this would disable, so they are excluded. The All Networks path is unchanged: it gets undefined, and both the cache key (`?? ''`) and the isOthersWallet gate (`!indexedAccountId`) treat that identically to absent. * fix: aggregate token network selector icon gray background (#12759) * feat: receive token search tokenization, network aliases and search-mode flatten (OK-59366) (#12771) * feat: tokenize receive token search keywords and add network aliases * feat: match receive search aliases and separators in local token filter * feat: flatten aggregate tokens in receive search results * feat: extend cross-network keyword stripping to all-networks receive search * fix: show asset symbol in aggregate network selector title (OK-60136) (#12868) * fix: show asset symbol in aggregate network selector title (OK-60136) * chore: remove redundant comment per review (OK-60136) * fix: filter delisted-network tokens in token search (OK-60860, partial pick of #13036) * chore: drop redundant string cast for v6.5.2 network type baseline * fix: normalize token search hit networkId before dedupe (OK-61630) Search hits that omit `info.networkId` were passed through untouched: under a scoped request they collided in the dedupe key and, on press, fell back to the selector's own network; under an all-networks request they could never resolve to a receive address. Stamp the request network onto scoped hits, drop unresolved hits on onekeyall, and keep the delisted-network catalog filter (OK-60860) in the same pure helper so it is unit-tested. Production responses currently always carry networkId; this closes the gap the type allows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q1n6Fx23wzn79FbSkdPSBn * fix: filter All Networks receive search for Others accounts (OK-61630) The Others-account compatibility filter only ran in single-network cross mode, so the onekeyall search in All Networks still listed tokens on chains an imported / watch-only / external credential cannot use. Pressing such a row found no account and fell into createAddress, which rejects non-HD/HW wallets, leaving a dead row. Apply the same filter the All-Networks account fan-out uses so every remaining row is selectable. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q1n6Fx23wzn79FbSkdPSBn * fix: keep receive token search results in step with the input (OK-61484) The selector used one trailing 1 s debounce for both the local filter and the backend request, and the stale-response guard only compared debounced keys. Pausing on an intermediate query ("usd" while editing "usdt" into "sol") fired its request, and the response was applied after the input had already moved on, showing the wrong list for up to 2 s. Split the debounce: the live key drives the list at 200 ms and the backend request waits a further 800 ms (same 1 s total, so the onekeyall search load is unchanged), clear stale results and show the trailing loader as soon as the live key changes, and drop any response whose keywords the input no longer reads. The two guards live in a pure helper with tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q1n6Fx23wzn79FbSkdPSBn * fix: zero-fill missing balances in receive token search rows (OK-61367) The Receive selector search merges three row sources: account tokens (fiat on record), server-known zero-balance tokens (`0 / $0.00`), and rows with no fiat record at all, i.e. backend keyword hits and aggregate sub rows on networks the account has no address on. The leaves render nothing for the last group since #12743, so one list showed real balances, zeros and blanks side by side and read as missing data. The search endpoint returns no balance fields without an address, so the record can only be seeded locally. Zero-fill the missing records for the Receive selector while a search is active: a context flag set by the TokenListView host, resolved in the per-field fiat hook so every leaf sees the same frozen zero record. Browse state and the sort (which reads the raw maps) are unchanged, and no other list opts in. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015jLbXXnKqkjwPJT7qzTE7r * fix: address receive token search review feedback - Scope the zero-fill placeholder to networks the selector fetch covered: under a single-network scope only rows on the selector network may read `0`, so the other-network rows the cross-network search adds keep a blank balance instead of asserting a holding nobody fetched. All Networks fans out over every enabled network the account has, so it is unchanged. - Key the Others-account network narrowing on the request being cross-network rather than on the keyword-stripping gates, so it still applies in All Networks with the LP filter on, and fold the filter state into the request identity so the unfiltered run that precedes `account` resolving cannot overwrite the narrowed one. - Write `liveSearchKeyRef` from onChangeText instead of the 200 ms debounced `searchKey`, closing the remaining window where a response for the previous query still passed the stale-response guard. - Fire the backend search at once when the effect re-runs for the same key (scope gate or filter context flip) instead of re-arming the 800 ms typing debounce, matching the pre-split behaviour for non-keystroke re-runs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012qMhxK97Kxk43oPWDAWrq4 --------- Co-authored-by: Franco <franco@onekey.so> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> * fix: bound the All Networks header hold and split the 6.5.2 home refresh hotfix OK-61576 (#13124) * fix: queue overlapping all-network refreshes and commit the home snapshot before asset analytics OK-61576 * fix: keep all-network DeFi readiness across same-owner init re-fires OK-61576 * fix: stop filtered selector results and stale Others scalars from persisting as network totals OK-61576 * fix: bound the All Networks header hold so a missing DeFi readiness cannot pin a stale total OK-61576 * fix: let the cache-only DeFi readiness probe run regardless of route focus OK-61576 * chore: trace all-network hook gate inputs and skipped runs OK-61576 * fix: address PR review feedback on the All Networks header hold OK-61576 - Do not arm the DeFi grace from a stale token commit while a DeFi run is in flight: a warm refresh never resets `updateAll`, but the DeFi run resets readiness at start and writes it back at finish, so the grace could drop DeFi from the header after 5 s while it was only reloading. Grace expiry is now sticky until DeFi reports for the owner. - Treat a fan-out whose every per-network request failed as a failed run: `continueOnError` resolves it with an empty result instead of throwing, which overwrote the retained last-good snapshot and left the consumer without an authoritative commit. Distinguish it from "no accounts" by the issued request count and restore the retained snapshot instead. * fix: keep the last same-owner DeFi value when the header hold releases OK-61576 After the DeFi grace releases the All Networks hold, the live total zeroed the DeFi part whenever readiness was unset, even though the overview atom still held this owner's last DeFi total (a warm refresh never clears it). The released total now includes that value when the atom is stamped with the current owner, and falls back to zero only when nothing is known. * chore: document the lastPublishedResultRef write invariant OK-61576 --------- Signed-off-by: ezailWang <jelly@onekey.so> Co-authored-by: Zen <mingzhen.fang@onekey.so> Co-authored-by: Kahn <0xkahnchan@gmail.com> Co-authored-by: Zhao <Charon.Dian@gmail.com> Co-authored-by: JellyWang <38491708+ezailWang@users.noreply.github.com> Co-authored-by: huhuanming <huanming@onekey.so> Co-authored-by: weatherstar <weather@onekey.so> Co-authored-by: Franco <franco@onekey.so> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* fix: recover Perps TWAP history and stalled order book subscriptions (OK-60985, OK-61108) (#12999) * fix: handle unknown Hyperliquid TWAP history statuses (OK-60985) The SDK status union omits `waitingForTrigger` and `stopped`, which the live API returns for trigger TWAPs. The exhaustive status map resolved to undefined, formatMessage threw on the missing id, and the error bubbled to the root boundary, blanking the whole Perps page on the TWAP history tab. Widen the status union in the shared SDK type re-export so exhaustive maps fail to compile instead of throwing at render time, and fall back to raw text for any status that has no translation key yet. * fix: stop a dropped Hyperliquid subscribe ack stalling the order book (OK-61108) A subscribe frame whose server ack never arrives blocked the reconcile for the SDK's default 10s timeout, so the order book stayed empty for ~11.5s after the app resumed and the user re-entered Perps on an uncached coin. The stall only cleared when something re-triggered updateSubscriptions, which made it look like the data loaded on swipe. Cap the WebSocket request timeout at 5s. Keep it at or above reconnect.connectionTimeout so frames rews buffers during a reconnect can still flush before the abort fires. Arm the critical subscription health check before awaiting _executeSubscriptionChanges instead of after it, so a stalled reconcile still gets a watchdog. It stays below the stale-critical branch, which bumps _subscriptionLifecycleVersion and would otherwise self-invalidate a timer that captured the version before the bump. * fix: localize TWAP history statuses OK-60985 (#13003) * fix: stop the subscription watchdog tearing down healthy subscriptions The critical health check is armed before _executeSubscriptionChanges awaits, so it can fire while the reconcile that armed it is still pending. Its rebuild queues every destroy behind the stalled mutation on the same per-key queue, so the healthy subscriptions are torn down while the recovery blocks on the very stall it was meant to interrupt. Skip the rebuild while a reconcile is in flight and re-arm instead, and re-arm in a finally so a _forceReconnectTransport reached inside the reconcile does not leave the check disarmed. * fix: close the watchdog race against a reconcile started mid-check The in-flight guard only ran before buildRequiredSubscriptionsMap awaited, so a reconcile starting during that await still reached the rebuild: a plain reconcile does not bump _subscriptionLifecycleVersion, so the post-await version check cannot catch it, and its subscribes have not landed yet, which biases the critical-type checks toward reporting them missing. Re-check the flag once the map resolves. Everything after it is synchronous, so that is the last point a concurrent reconcile can slip in before the cleanup tears its subscriptions down. --------- Co-authored-by: Kahn <0xkahnchan@gmail.com> Co-authored-by: Leon <lixiao.dev@gmail.com> * feat: register the Entropy hip-3 perps dex (OK-61073, OK-60689) (#13029) * feat: register the Entropy hip-3 perps dex (OK-61073) Entropy is hyperliquid perp dex index 10, prefix `io`. Registering it in SUB_DEX_LIST is enough for the market list, token selector, favorites, universal search routing and deep links, which all read the registry. The `io` prefix is the first one that shadows a real main-DEX symbol: findDexPrefix matches bare prefixes so legacy separator-free links keep working, which turns `?token=IOTA` into a guess of `io:TA`. The universe check already rejects that guess, but when the cache is incomplete -- a cold-start deep link, exactly when this runs -- the unproven guess was kept and the page silently fell back to the previous coin. Only `xyz` ever shipped separator-free links, from before encodeCoinForUrl kept the separator, so its guess still wins without a universe. Prefixes registered since fall back to the literal token instead. `cash` (dex 7, shadows CASHCAT) would have hit the same trap. The URL codec moves to a .utils sibling so it is testable without pulling in backgroundApiProxy, matching PerpsGlobalEffects.utils. * fix: pick the perps url fallback by main-dex shadowing, not link history The fallback answers which reading could name a real market when the universe cannot settle it, but it was keyed on whether a prefix ever shipped separator-free links. Those criteria agree on xyz and io and disagree on para: no main-DEX symbol starts with PARA, so PARAUNITREE is no market at all while para:UNITREE resolves. A hand-written link went from always right to always wrong, in exactly the window this PR widens -- the fourth registry slot rejects every persisted cache, so the first cold start after the hot update runs with an incomplete universe. Key it on main-dex shadowing instead: only a prefix a real symbol starts with has a literal reading worth keeping. Narrow the prefix type so the list cannot drift from SUB_DEX_LIST. * feat: bump perps asset type version to 3 for the io dex Gate a newly registered dex behind the version so the server can keep it out of responses to clients that predate it. Search drops an unregistered prefix on its own, but that guard does not cover every surface, so the version is what actually keeps a new dex away from older builds. * fix: show the dex badge on market watchlist perps rows The watchlist renders perps rows through its own branch in the market columns, which carried the leverage badge but never the dex one, so an xyz or para market was indistinguishable from a main-DEX one there. Every other perps surface shows it. TokenIdentityItem takes a perpsDexLabel prop for this, but perps rows never reach it -- the branch above returns first -- and no caller ever passed it, so the prop reads as wired while rendering nothing. Derive the label from record.perpsCoin, which already holds the full prefixed coin. Native delegates to the mobile columns, so both platforms are covered. * fix: localize Entropy perps dex description OK-61073 (#13034) * fix: show the dex badge on mobile market watchlist rows Narrow web and native render the watchlist through MobileMarketWatchlistFlatList -> TokenListItem -> TokenIdentityItem, not through the market columns, so the badge added to useColumnsMobile never reached them. TokenListItem already forwards maxLeverage and perpsSubtitle, which is why those rows show the leverage badge and the company name but no dex tag. It just never passed perpsDexLabel, so TokenIdentityItem rendered PerpDexBadge with an undefined label and it bailed out. Derive the label from item.perpsCoin the way the columns do. Spot rows carry no perpsCoin and keep rendering nothing. * fix: send assetTypeVersion on the market and perps config requests The backend gates a newly registered dex behind `assetTypeVersion`, and a request that omits it is served the dataset from before that dex existed. Only universal search ever sent it, so search was the one surface that could see `io` while the market list, the watchlist, the token selector and the perps config all silently got the pre-`io` payload. Verified against the test server: `token-list?category=all` returns 302 tokens with no `io:` row, the same call with `assetTypeVersion=3` returns 305 including io:ANTH, io:SNDK and io:NBIS; `perp-config` returns an empty pre-ipo tab, and with the parameter it carries the configured entry. Move PERPS_ASSET_TYPE_VERSION next to SUB_DEX_LIST so registering a dex and bumping the version are read together. Keeping it private to the search service is what let two callers miss it. * fix: use FrontendMarket tif for hyperliquid market close * fix: align hyperliquid market orders to FrontendMarket tif * fix: stabilize chart limit order button width and show order value * refactor: restore original side stats guards in chart limit order --------- Co-authored-by: Kahn <0xkahnchan@gmail.com> Co-authored-by: Leon <lixiao.dev@gmail.com> * fix: refresh DeFi network config cache OK-61713 (#13126) * fix: DeFi share links, banner looping, risk gate coverage and in-app … (#13135) * fix: DeFi share links, banner looping, risk gate coverage and in-app tip links (OK-61675, OK-61479, OK-61516, OK-61515, OK-61348, OK-59196, OK-61325) - earn share links: derive the :network slug from the preset network shortcode instead of a hardcoded table, so Katana and friends stop generating /earn/unknown/... that the detail page rejects; the old ethereum/solana/aptos/cosmos spellings stay valid on parse - earn home banner: add an opt-in `infinite` mode to Carousel so the banner wraps around and never sits at a content edge, where the platform pager hands the gesture to its parent; exclude the banner area from the header's tab-switch pan, which sat on top of it; drop the mid-gesture scrollEnabled lock on the outer pager, which never covered that pan anyway - risk disclaimer: extend the one-time gate from earn deposit to every remaining trade entry (earn withdraw/claim and their approvals, all borrow actions and the borrow approve step, DeFi Portfolio one-click actions), and return a boolean from the trade hooks so a declined disclaimer keeps the typed amount and releases the submit guards instead of sticking on loading - risk notice / protocol tips dialogs now close before a link opens, instead of staying stacked behind the page - add the <urlInApp> rich-text tag: opens tip links in the in-app webview with the dapp bridge on, re-checking every top-frame navigation and showing the live host; <url> keeps going to the system browser. Server-side gating ships separately in server-service-earn, keyed on jsbundle version because the app version header does not move for a hot update Signed-off-by: ezailWang <jelly@onekey.so> * fix: Earn banner gesture handling and protocol tips overflow Carousel (Earn home banner): - Fix the white screen when a finger lands during an auto-scroll. The infinite-loop clone swap called setPageWithoutAnimation while the pager was still driven by the gesture, which UIPageViewController asserts on and ViewPager2 rejects. The jump is now held until onPageScrollStateChanged reports idle, and an autoplay beat is skipped instead of turning the page out from under a finger. - Stop a swipe from opening the card it ends on. The native pager claims the horizontal pan, so a Pressable inside a page never receives the move that would cancel it and sees only down and up. The carousel exposes useCarouselPressSuppressor and EarnHomeBanner consults it before navigating. - Resume autoplay after navigating away and back. The timer was paused by onPressIn and only restarted by onPressOut, which never arrives once the screen is leaving, and the IntersectionObserver fallback is web-only. Autoplay now follows useIsFocused, matching composite/Banner. - Keep the native-only onPageScrollStateChanged prop off the DOM node in the web pager shim. Protocol tips: - <urlInApp> links open a Discovery tab instead of the in-app WebView modal, falling back to the system browser on web and the extension, where Discovery does not exist. - Long tips scroll inside the dialog instead of overflowing the viewport on phone and desktop; drag-to-close is disabled there so the sheet stops competing with the inner scroll for the same vertical gesture. Signed-off-by: ezailWang <jelly@onekey.so> * fix: lint Signed-off-by: ezailWang <jelly@onekey.so> * fix: review p1 Signed-off-by: ezailWang <jelly@onekey.so> --------- Signed-off-by: ezailWang <jelly@onekey.so> * fix: Perps close-position price readiness, cold-start banner shift, favorites order sync and TWAP tab shadow flash(OK-61259 OK-61504 OK-61486 OK-61520) (#13128) * fix: guard rapid Perps close-position taps (OK-61259) handleClosePosition opened a new dialog on every tap, so fast taps stacked several dialogs each holding its own position snapshot, and the confirm button's disabled state landed one render too late to stop a same-frame double tap. Only one close dialog is kept alive at a time and the submit is guarded by a ref. * fix: prevent iOS Perps layout shift on cold start (#13097) (OK-61504) (cherry picked from commit 3c945b8) * fix: follow the favorites bar drag order in the footer ticker and token selector (OK-61486) usePerpsFavorites returned membership order, so the footer ticker never saw the persisted drag sequence, and the token selector applied the default volume sort on top of it. Favorites now flow through the persisted sequence everywhere, and the favorites tab only sorts after an explicit header click on that tab, matching how the category tabs already behave. * fix: stop the TWAP sub-tab switch flashing the fixed-column shadow (OK-61520) Each TWAP sub-tab mounts its own table seeded with the shadow visible, and the overflow measurement ran after paint, so a full-width table flashed the shadow for one frame on every switch. Measuring in a layout effect settles the shadow before the browser paints. * fix: keep the close-position guard armed through the dialog exit animation (OK-61259) onClose() does not await the 300ms exit animation, so releasing the guard in finally re-enabled Confirm while the dialog was still mounted; a tap in that window would send a second reduce-only order. The guard now resets only on early return or failure and otherwise dies with the dialog. * refactor: reuse the favorites sequence helper in the favorites bar (OK-61486) * fix: require a fresh close-position price (OK-61259) iOS cold start: positions, order book and the trading buttons restore from the main-runtime cold-start cache, but perpsAllMidsAtom has no cache and only fills once the background WebSocket delivers the first allMids a few seconds later. The close dialog turned the missing price into '0', Boolean('0') enabled Confirm, and the submit path rejected '0' with "Unable to get current market price" without sending an order. main runtime: a missing, zero or non-finite mid is now undefined, so Confirm stays disabled with a spinner until the price arrives. bg runtime: the submit resolves the price from the background allMids cache when it is under 5s old, otherwise from a per-dex REST allMids snapshot; the REST result is not written to the shared cache because it covers one dex only. Evidence: log of OneKey 6.6.0 build 2026082600 bundle 20484813 on iOS 26.6, first allMids reached bg 4.2s after launch, Confirm tapped 2s before that, no ordersClose RPC followed. * fix: keep the limit close price editable and share the background price gate (OK-61259) Turning a missing mid into undefined made the limit PriceInput's disabled={!midPrice} bite for the first time, locking the input and hiding the Mid shortcut during the cold-start window; the input now stays editable and only the Mid action dims. The market Confirm gate also only watched the main-runtime atom while the submit already resolved prices in the background, so the dialog now polls the same background source every 2s; when that source has no price either, the spinner stops and the existing toast tells the user, leaving the limit path as the exit. * fix: resolve Close All prices in the background and fail instead of dropping positions (OK-61259) closeAllPositions read mids from the main-runtime atom only and silently filtered out positions without one, so during the cold-start window it could close a subset while the inner ordersClose toast reported success. Missing mids now go through getMarketOrderReferencePrice, and any position still without a price aborts the whole action with a visible error. * fix: keep the fixed-column shadow transition off until the first measured paint (OK-61520) Reading scrollWidth in the layout effect forces a style flush with the seeded initialVisible shadow, so the CSS transition animated the seeded to measured change even though the seeded frame itself never painted. The transition now stays 'none' until the measured state has been painted (double rAF), then turns on for real scroll-driven changes. * fix: price every Close All position through the background owner (OK-61259) closeAllPositions took the main-runtime atom's mid whenever one existed and only asked the background for missing coins. That atom carries no age marker, so a stalled ALL_MIDS stream could bound the whole batch around an arbitrarily stale midpoint. Every coin now resolves through getMarketOrderReferencePrice, the same freshness-checked owner the single close submit uses; the all-or-nothing abort stays. * chore: mark the close-price error literal for i18n follow-up (OK-61259) --------- Co-authored-by: huhuanming <huanming@onekey.so> * feat: withdraw Perps USDC over Hyperliquid's CCTP rail(OK-61320) (#13119) * feat: withdraw Perps USDC over Hyperliquid's CCTP rail (OK-61320) Hyperliquid moved USDC to Circle CCTP and marked the legacy Arbitrum bridge deprecated. Follow the rail it serves from info/usdcRouting, which drops the withdrawal fee from $1 to $0.20 and leaves a switch back to the bridge without another release. A rail already confirmed live wins over the fallback, so one failed lookup cannot quietly downgrade a user to the deprecated bridge. `sendToEvmWithData` needs an explicit source balance where `withdraw3` let Hyperliquid pick one, so mirror perpsComputedAccountValueAtom and source from spot for unified and portfolio-margin accounts. CCTP withdrawals reach the ledger as a `send` to the HyperEVM system address rather than a `withdraw`, so teach net deposits and the account history row about that shape. Without it the balance drop reads as a trading loss and the row renders as money coming in. Net deposits counts both directions across that address so a Core/HyperEVM round trip nets to zero. * feat: add destination selection for perps USDC withdrawals (#13125) * feat: support multi-chain perps USDC withdrawals * fix: keep perps withdrawal destinations selectable * fix: drop the unreachable inbound leg from perps net deposits EVM -> Core credits arrive as a `spotTransfer` from the HyperEVM system address, never as a `send` from it, so the inbound arm never matched. Counting them properly would double up against the `accountClassTransfer` branch, which already treats spot as outside this reducer's scope, so the outbound leg stands alone and the comment now says so instead of claiming a round trip nets to zero. * fix: make perps net deposits match its own definition The stat reads "total deposits minus total withdrawals", but it also counted `accountClassTransfer`, which only moves USDC between the perp and spot balances of one account. That was already wrong by its own definition, and it became a double subtraction once a withdrawal could be sourced from spot: once on the way to spot, again on the way out to HyperEVM. Also stop treating an unreadable `usdcRouting` response as a genuine switch to the legacy bridge. `requestLoggedHyperLiquidTransport` reports `{status:'err'}` bodies but still returns them, so one malformed answer used to be cached as `bridge` for five minutes and overwrite the last confirmed rail, defeating the guard that exists to stop exactly that. * fix: refresh the perps withdrawal rail while the form is open A rail that flips mid-session made submission fail with "route changed. Review the updated fee and try again", but the modal resolved the rail once per open, so the retry it asks for could never succeed until the user closed and reopened the form. It now refreshes on the same tick as the fee quote, which the background cache already serves for free most of the time. * fix: explain why a withdrawal inside the gas reserve is rejected Withdrawals validate against the balance minus the Core -> EVM gas reserve, but the insufficient-balance check still compared against the untrimmed balance, and that untrimmed number is what the form displays as available. Typing it left the submit button disabled with no message and no way out. Both now use the same ceiling, so the amount inside the reserve reports insufficient balance. * fix: count HyperEVM credits back into perps net deposits Dropping `accountClassTransfer` committed this stat to whole-account scope and removed the reason the inbound leg was left out. Without it a Core/HyperEVM round trip subtracts on the way out and never adds on the way back, so a user who withdraws to HyperEVM and moves the funds back sees the stat drift permanently negative — and HyperEVM is now one of the offered destinations. The credits arrive as a `spotTransfer` sent by the system address. * test: cover perps net deposits accounting The reducer moved three times under review and had no test, on a stat users read as a money figure. Extracted it to portfolioStats as a pure function and covered the cases that changed: a CCTP withdrawal counting as an outflow whichever balance it is sourced from, a Core/HyperEVM round trip netting to zero, and transfers between an account's own balances staying out of it. * fix: stop the perps withdrawal fee flickering on destination change Switching destinations invalidated the quote by key, so the row rendered a placeholder between the old and new value — visible even between two destinations charging the same amount. Each destination's fee is known before the quote returns, so the row now previews it and only the value itself changes. The periodic refresh also rebuilt its quote object every tick; it now keeps the previous one when the numbers are unchanged. Submission still binds to the confirmed quote, not the preview. * chore: tighten the comments added for the CCTP withdrawal work Several ran to four or six lines restating what the code does. Kept the reason each block exists and dropped the narration. * fix: keep the perps withdraw button steady when the destination changes The button was gated on the confirmed fee quote, which is invalidated by key the moment a destination changes, so it greyed out for the frame before the new quote landed. It now gates on the same preview the fee row uses, and the wait for the confirmed quote moved into the submit handler, which still binds submission to a server-confirmed fee. * fix: stop the CCTP fee estimate marker blinking on destination change Only CCTP destinations read their fee from chain, so only they could hand the row a confirmed quote that differs from the preview: same number, but flagged as an estimate, which added and removed a prefix and read as a flicker. HyperEVM never showed it because its fee is local. The marker told the user nothing the number did not — a failed read falls back to the same value, and submission re-reads it and refuses on drift — so the row now just shows the amount. * fix: never submit a perps withdrawal against an unseen fee Gating the button on the fee preview let a tap land inside the window where the destination had changed but its quote had not, and the submit handler then fetched the live fee and signed with it. The CCTP fee comes out of the principal, so the user could be charged a number the row never showed. The button waits for the confirmed quote again, and the preview is marked as the estimate it is. * fix: quote every withdrawal destination up front The row and the submit button both waited on a per-destination quote that only started loading once the destination changed, so every switch spent a frame without one: the fee fell back to the estimate and the button greyed out. The quotes are now kept per destination and requested for all of them when the form opens, so a switch lands on one that is already confirmed. The rail is served server-side, so this runs once per open. * fix: replace the withdrawal fee placeholder with a loading state Quoting every destination up front already closed the gap a destination switch used to leave, so the estimate preview only survived on form open, where it added a second transition: placeholder, then estimate, then the confirmed number. The estimate marker appearing and vanishing is what read as a flicker. The row now shows a skeleton until the quote lands, matching the withdrawable row above it, and quotes outlive the modal so reopening the form does not blank it again while it refetches. --------- Co-authored-by: Kahn <0xkahnchan@gmail.com> * fix: normalize token search hit networkId before dedupe (OK-61630) Search hits that omit `info.networkId` were passed through untouched: under a scoped request they collided in the dedupe key and, on press, fell back to the selector's own network; under an all-networks request they could never resolve to a receive address. Stamp the request network onto scoped hits, drop unresolved hits on onekeyall, and keep the delisted-network catalog filter (OK-60860) in the same pure helper so it is unit-tested. Production responses currently always carry networkId; this closes the gap the type allows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q1n6Fx23wzn79FbSkdPSBn * fix: filter All Networks receive search for Others accounts (OK-61630) The Others-account compatibility filter only ran in single-network cross mode, so the onekeyall search in All Networks still listed tokens on chains an imported / watch-only / external credential cannot use. Pressing such a row found no account and fell into createAddress, which rejects non-HD/HW wallets, leaving a dead row. Apply the same filter the All-Networks account fan-out uses so every remaining row is selectable. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q1n6Fx23wzn79FbSkdPSBn * fix: keep receive token search results in step with the input (OK-61484) The selector used one trailing 1 s debounce for both the local filter and the backend request, and the stale-response guard only compared debounced keys. Pausing on an intermediate query ("usd" while editing "usdt" into "sol") fired its request, and the response was applied after the input had already moved on, showing the wrong list for up to 2 s. Split the debounce: the live key drives the list at 200 ms and the backend request waits a further 800 ms (same 1 s total, so the onekeyall search load is unchanged), clear stale results and show the trailing loader as soon as the live key changes, and drop any response whose keywords the input no longer reads. The two guards live in a pure helper with tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q1n6Fx23wzn79FbSkdPSBn * fix: zero-fill missing balances in receive token search rows (OK-61367) The Receive selector search merges three row sources: account tokens (fiat on record), server-known zero-balance tokens (`0 / $0.00`), and rows with no fiat record at all, i.e. backend keyword hits and aggregate sub rows on networks the account has no address on. The leaves render nothing for the last group since #12743, so one list showed real balances, zeros and blanks side by side and read as missing data. The search endpoint returns no balance fields without an address, so the record can only be seeded locally. Zero-fill the missing records for the Receive selector while a search is active: a context flag set by the TokenListView host, resolved in the per-field fiat hook so every leaf sees the same frozen zero record. Browse state and the sort (which reads the raw maps) are unchanged, and no other list opts in. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015jLbXXnKqkjwPJT7qzTE7r * fix: address receive token search review feedback - Scope the zero-fill placeholder to networks the selector fetch covered: under a single-network scope only rows on the selector network may read `0`, so the other-network rows the cross-network search adds keep a blank balance instead of asserting a holding nobody fetched. All Networks fans out over every enabled network the account has, so it is unchanged. - Key the Others-account network narrowing on the request being cross-network rather than on the keyword-stripping gates, so it still applies in All Networks with the LP filter on, and fold the filter state into the request identity so the unfiltered run that precedes `account` resolving cannot overwrite the narrowed one. - Write `liveSearchKeyRef` from onChangeText instead of the 200 ms debounced `searchKey`, closing the remaining window where a response for the previous query still passed the stale-response guard. - Fire the backend search at once when the effect re-runs for the same key (scope gate or filter context flip) instead of re-arming the 800 ms typing debounce, matching the pre-split behaviour for non-keystroke re-runs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012qMhxK97Kxk43oPWDAWrq4 * fix: bound the All Networks header hold and split the 6.5.2 home refresh hotfix OK-61576 (#13124) * fix: queue overlapping all-network refreshes and commit the home snapshot before asset analytics OK-61576 * fix: keep all-network DeFi readiness across same-owner init re-fires OK-61576 * fix: stop filtered selector results and stale Others scalars from persisting as network totals OK-61576 * fix: bound the All Networks header hold so a missing DeFi readiness cannot pin a stale total OK-61576 * fix: let the cache-only DeFi readiness probe run regardless of route focus OK-61576 * chore: trace all-network hook gate inputs and skipped runs OK-61576 * fix: address PR review feedback on the All Networks header hold OK-61576 - Do not arm the DeFi grace from a stale token commit while a DeFi run is in flight: a warm refresh never resets `updateAll`, but the DeFi run resets readiness at start and writes it back at finish, so the grace could drop DeFi from the header after 5 s while it was only reloading. Grace expiry is now sticky until DeFi reports for the owner. - Treat a fan-out whose every per-network request failed as a failed run: `continueOnError` resolves it with an empty result instead of throwing, which overwrote the retained last-good snapshot and left the consumer without an authoritative commit. Distinguish it from "no accounts" by the issued request count and restore the retained snapshot instead. * fix: keep the last same-owner DeFi value when the header hold releases OK-61576 After the DeFi grace releases the All Networks hold, the live total zeroed the DeFi part whenever readiness was unset, even though the overview atom still held this owner's last DeFi total (a warm refresh never clears it). The released total now includes that value when the atom is stamped with the current owner, and falls back to zero only when nothing is known. * chore: document the lastPublishedResultRef write invariant OK-61576 * fix: preserve HIP-3 DEX badge in market watchlist * fix: resolve sync CI and review issues --------- Signed-off-by: ezailWang <jelly@onekey.so> Co-authored-by: Zen <mingzhen.fang@onekey.so> Co-authored-by: Kahn <0xkahnchan@gmail.com> Co-authored-by: Zhao <Charon.Dian@gmail.com> Co-authored-by: JellyWang <38491708+ezailWang@users.noreply.github.com> Co-authored-by: huhuanming <huanming@onekey.so> Co-authored-by: weatherstar <weather@onekey.so> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>


OK-61576
Summary
Hotfix split of #13103 for
release/v6.5.2, scoped to what the customer's iOS log (6.5.2, iPhone 15 Pro) actually shows, plus a fix for the gate that caused the reported symptom.What the log shows (device time):
shouldHoldCurrentConfirmedBalanceto a persisted total until BOTH token and DeFi readiness are true. Token readiness is trivially true after any network settles, so the gate is effectively "DeFi ready or freeze".DeFiListBlockall-network hook run. Across 8 cold starts that day, that hook ran twice (18:43:49, 23:33:03). Those are exactly the two moments the header refreshed; the user's screenshot (header $21,880 vs one row $25,988) is the frozen state in between, while the token list already updated through progressive LWW merges.getAllHdHwQrWalletsfor 93 s while iOS suspended the app (23:30:33 to 23:32:07).Kept from #13103 (evidence-backed):
useAllNetwork: reserve the debounce window so a second manual refresh queues instead of spawning a runner; queued reruns keep their must-run config; stale-runner owner bail-out; retained result cleared on accepted runs and restored on failure (signature-checked).TokenListBlock: asset-status analytics moved after the authoritative commit; duplicate forced fan-out onAddDBAccountsToWalletremoved;clearRetainedResultOnAcceptedRun: true.DeFiListBlockdeFiOverviewInitPlan: same-owner init re-fires (currency map refresh) no longer reset readiness.TokenSelector: a filtered selector response is not persisted as the canonical network total.HomeOverviewContainer: Others accounts persist the compound-key value instead of the transient scalar.New in this PR:
resolveHomeOverviewBalanceHold: the hold is bounded. Once the token side has committed a complete snapshot (accountWorth.updateAll), DeFi gets a 5 s grace window; after that the live total is shown with the DeFi value currently known. The confirmed-balance persistence still requires full readiness, so a tokens-only total is never persisted as confirmed.shouldAlwaysFetch, sousePromiseResult's route-focus gate cannot skip the local cache probe that produces DeFi readiness.homeTokenListRefreshTracenow recordsall-network-hook-gate(disabled / route focus / lock / shouldAlwaysFetch) andall-network-hook-run-skippedwith askipReason, so a hook that never starts can be explained from device logs next time.Deliberately excluded: the
IAssetSnapshotMetafreshness-marker system (ServiceToken minting, actions / ServiceAccountProfile / SimpleDb admission gates, LocalTokens admission). The log contains no observed out-of-order overwrite; the only theoretical window is the 93 s-delayed gen-4 commit landing after gen-5 progressive merges, which self-heals on the next commit and is largely removed by the two fixes above. It stays in #13103 to soak onx.Verification
yarn jestforallNetworkRunResultUtils,deFiOverviewInitPlan,homeOverviewBalanceHold,TokenListBlock.portfolioSync,DeFiListBlock.cacheOnlyGate: 27 tests pass.yarn agent:check --profile commit: lint, tsc pass.all-network-hook-gate/all-network-hook-run-skippedwithreason:"defi"in logs).all-network-authoritative-commitafter the last run.Related: #13103 (full change set incl. freshness markers), OK-61576.