Skip to content

fix: bound the All Networks header hold and split the 6.5.2 home refresh hotfix OK-61576 - #13124

Merged
originalix merged 10 commits into
release/v6.5.2from
fix/wallet-home-header-hold-v6.5.2
Sep 4, 2026
Merged

fix: bound the All Networks header hold and split the 6.5.2 home refresh hotfix OK-61576#13124
originalix merged 10 commits into
release/v6.5.2from
fix/wallet-home-header-hold-v6.5.2

Conversation

@weatherstar

@weatherstar weatherstar commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

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):

  • The All Networks header is pinned by shouldHoldCurrentConfirmedBalance to 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".
  • DeFi readiness is written only inside the cache-only DeFiListBlock all-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.
  • Two secondary defects made refreshes look dead: two manual refreshes inside the 1 s debounce (23:29:57/58 and 23:32:07/08) let the second runner's nonce discard the first fan-out and the rerun was skipped as redundant; and the authoritative commit waited on getAllHdHwQrWallets for 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 on AddDBAccountsToWallet removed; clearRetainedResultOnAcceptedRun: true.
  • DeFiListBlock deFiOverviewInitPlan: 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.
  • The cache-only DeFi hook passes shouldAlwaysFetch, so usePromiseResult's route-focus gate cannot skip the local cache probe that produces DeFi readiness.
  • Diagnostics: homeTokenListRefreshTrace now records all-network-hook-gate (disabled / route focus / lock / shouldAlwaysFetch) and all-network-hook-run-skipped with a skipReason, so a hook that never starts can be explained from device logs next time.

Deliberately excluded: the IAssetSnapshotMeta freshness-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 on x.

Verification

  • yarn jest for allNetworkRunResultUtils, deFiOverviewInitPlan, homeOverviewBalanceHold, TokenListBlock.portfolioSync, DeFiListBlock.cacheOnlyGate: 27 tests pass.
  • yarn agent:check --profile commit: lint, tsc pass.
  • Device QA still needed (iOS + desktop, All Networks, HW wallet):
    1. Cold start on the Tokens tab without visiting DeFi: header must leave the persisted value within ~5 s and match the list (look for all-network-hook-gate / all-network-hook-run-skipped with reason:"defi" in logs).
    2. Double-tap manual refresh inside 1 s: exactly one fan-out plus one queued must-run, and an all-network-authoritative-commit after the last run.
    3. Refresh, background the app for >30 s, foreground: header and list converge on the same snapshot.

Related: #13103 (full change set incl. freshness markers), OK-61576.

@weatherstar
weatherstar marked this pull request as ready for review September 2, 2026 07:21
weatherstar added a commit that referenced this pull request Sep 2, 2026
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
@sidmorizon

Copy link
Copy Markdown
Contributor

@codex review

@sidmorizon

Copy link
Copy Markdown
Contributor

@codex security review

@sidmorizon

Copy link
Copy Markdown
Contributor

@cursoragent review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-02T07:45:51.033507Z e76a061 Manual request
🔒 Security Review Completed 2026-09-02T07:41:56.682737Z e76a061 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cursor

cursor Bot commented Sep 2, 2026

Copy link
Copy Markdown

PR #13124 代码审查报告

审查基准是 origin/release/v6.5.2...HEAD(hotfix,不是 origin/x)。14 个文件,+900 / −162。Codex 审查仍在跑,本报告是独立主审。

审查概要

  • 变更范围: 14 个文件, +900 / -162 行
  • 风险等级: Medium
  • 涉及平台: Mobile(iOS 主路径)/ Desktop / Web / Extension(Home All Networks 头图与 refresh 队列)
  • Codex 交叉验证: ⏭️ 未启用
  • PR 评论分析: ✅ 已分析 (5 条评论, 其中 2 条来自 Bot)

评分

维度 得分 说明
🔒 安全性 9/10 无密钥/助记词/签名路径;新增 trace 只有 networkId、布尔门闩、计数
💎 代码质量 7.5/10 纯函数拆得干净,队列/失败恢复写得很完整;updateAll 与 5s grace 的耦合有行为缺口
🏛️ 架构合理性 8.5/10 包边界正确;相对 #13103 的裁剪合理;shouldAlwaysFetch 打在 cache-only DeFi 上是对的杠杆
✅ 完整性 7.5/10 新 helper 单测覆盖好;hold 与 updateAll 被 merge 清掉的路径没有测;两处源码字符串测试很脆
总分 8.2/10 ✅ 可直接合入(建议先看清 grace 闪回,不阻塞合入)

PR 评论分析

来源 类型 发现 判定 说明
@sidmorizon 👤 Human @codex review / @codex security review / @cursoragent review 📋 Already Covered 触发审查,无实质意见
chatgpt-codex-connector 🤖 Bot Codex Code + Security Review running 📋 Already Covered 截至审查时仍在跑,尚无 finding
cursor[bot] 🤖 Bot Taking a look ❌ Noise 本审查的占位回复

发现的问题

[🟡 中] [🟠 Medium] updateAll 会被 progressive merge 清掉,5s grace 和注释不一致

文件: packages/kit/src/states/jotai/contexts/accountOverview/actions.ts:69,消费方 HomeOverviewContainer.tsx:737 / TokenListBlock.tsx:1165
类型: 运行时
描述: resolveHomeOverviewBalanceHoldaccountWorth.updateAll === true 当成「完整快照已提交」(注释写明包含 cache hydrate)。但 updateAccountWorthmerge: true 时直接写入 payload.updateAll,而 per-network 累加(TokenListBlock.tsx:1165)不传 updateAll,于是变成 undefined

实际时序:

  1. cache hydrate 设 updateAll: true → grace 启动
  2. 第一条 live 网络 settle → updateAll 被清掉 → shouldArmDeFiGrace 变 false → effect 把 deFiGraceExpired 重置
  3. 头图重新 hold 到旧的 confirmed
  4. 只有 authoritative commit(updateAll: true)之后,5s 窗口才真正开始

对 OK-61576 的「整段 session 冻住」这仍然有效:commit 后 5s 会放行。但注释说的「cache hydrate 后开始计时」几乎不会发生。更糟的是慢设备(HW / 弱网)若 hydrate 后 5s 内还没有第一条 settle,头图会先放出 live cache,再被第一条 merge 闪回到旧 confirmed,直到 fan-out commit + 再等 5s。

测试 does not let an expired grace release a still-progressive token side 覆盖了「从未 committed」;没有覆盖「committed → grace 到期 → merge 清掉 updateAll」。

修复建议: 不要用会被 merge 覆盖的 updateAll 兼做 grace 开关。任选其一:

  • grace 在 authoritative commit 后启动(cache hydrate 不 shouldArmDeFiGrace),或
  • 用 owner 级的 hasCommittedSnapshotRef,merge 时不要清掉,或
  • merge 时保留已有 updateAllupdateAll: payload.updateAll ?? existing.updateAll——但这会让 cache hydrate 之后的 progressive 阶段也开始爬升,和 hold 的初衷冲突,不建议。

[🟢 低] [🔵 High] debounce 预约只对 TokenListBlock 生效

文件: packages/kit/src/hooks/useAllNetwork.ts:1188 / DeFiListBlock.tsx:947
类型: 运行时
描述: debouncePendingCountRef 的增减、以及 runWithQueue 里「debounce 窗口内改排队」都包在 clearRetainedResultOnAcceptedRun 里。只有 TokenListBlock 传了 true。DeFi / NFT 的 1s 内二次手动刷新仍会再开一条 usePromiseResult runner,用新 nonce 作废第一条 fan-out。和客户日志里的 token 双击路径一致,所以 hotfix 范围说得通,但 PR 描述里「queued reruns」写得像通用修复。

修复建议: 后续把预约从 clearRetainedResultOnAcceptedRun 解耦,或给 DeFi 的 useAllNetworkRequests 也打开该旗标。


[🟢 低] [🔵 High] cache-only / portfolio-sync 测试是源码字符串匹配

文件: DeFiListBlock.cacheOnlyGate.test.tsTokenListBlock.portfolioSync.test.ts
类型: 规范
描述: 用 readFileSync + indexOf / toContain 锁字符串位置。重构即红,也不验证 shouldAlwaysFetch 是否真的绕过 focus gate。新的 homeOverviewBalanceHold / allNetworkRunResultUtils / deFiOverviewInitPlan 单测是正经的。

修复建议: 能抽纯函数的继续抽;这两处 syntactical lock 当回归哨兵可以留,但不要当成行为覆盖。


修改清单

优先级 置信度 文件 类型 描述 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 规范 源码字符串测试

测试建议

  1. Tokens 冷启动、不进 DeFi:确认 all-network-hook-gate 出现 shouldAlwaysFetch:true(reason:defi),随后有 all-network-hook-run-start;头图在 token commit 后约 5s 内离开旧 confirmed,和列表收敛。
  2. 1s 内双击刷新:token 侧应只有一次 fan-out + 一次 queued must-run,最后有 all-network-authoritative-commit
  3. 刷新后后台 >30s 再回前台:头图和列表同一快照。
  4. 慢设备 / HW:cache hydrate 后故意拖住第一条 settle >5s,看头图会不会先放出再闪回旧值。
  5. 大额 DeFi 仓位:grace 到期而 DeFi 仍未 ready 时,头图会按 tokens + 0 DeFi 显示(resolvedBalanceString!isCurrentAccountDeFiReady 时把 DeFi 当 0),然后再跳上来。这是刻意取舍,不要当回归失败,但 QA 要知情。
  6. TokenSelector 过滤模式:打开带 filter 的选择器后回到 Home,网络总值不应被子集覆盖。
  7. Others 账户createAtNetworkWorth 标量在 merge 中途变脏时,SimpleDB 应只写 compound-key 上的绝对值。

没有达到 inline comment 门槛的 finding(需要 🔴,或 🟡 且 🔵 High),所以不往 PR 上挂行内评论。若要我针对 grace/updateAll 闪回补一刀修复,直接说即可。

Open in Web Open in Cursor 

@chatgpt-codex-connector

Copy link
Copy Markdown

Security review completed. No security issues were found in this pull request.

Reviewed commit: e76a06155c

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Comment thread packages/kit/src/views/Home/pages/HomeOverviewContainer.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/kit/src/hooks/useAllNetwork.ts
Comment thread packages/kit/src/views/Home/pages/homeOverviewBalanceHold.ts
Comment thread packages/kit/src/hooks/useAllNetwork.ts
- 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.
Comment thread packages/kit/src/views/Home/pages/HomeOverviewContainer.tsx
@originalix originalix added the release-ready This PR should be included in the next release label Sep 4, 2026
@originalix
originalix merged commit 3166201 into release/v6.5.2 Sep 4, 2026
17 of 18 checks passed
@originalix
originalix deleted the fix/wallet-home-header-hold-v6.5.2 branch September 4, 2026 08:40
originalix added a commit that referenced this pull request Sep 4, 2026
* 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>
originalix added a commit that referenced this pull request Sep 5, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-ready This PR should be included in the next release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants