Skip to content

feat: Swap token carry and stock-tag search fix (OK-55190, OK-60609) - #12932

Closed
zhaono1 wants to merge 12 commits into
xfrom
fix/zhaono1-081803
Closed

feat: Swap token carry and stock-tag search fix (OK-55190, OK-60609)#12932
zhaono1 wants to merge 12 commits into
xfrom
fix/zhaono1-081803

Conversation

@zhaono1

@zhaono1 zhaono1 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

OK-55190 OK-60609


Summary

  • Carry a manually selected target token once between ordinary Swap and native Pro without overwriting either side on plain tab switches (OK-55190).
  • Swap -> Pro prefers ToToken, falls back to FromToken when ToToken is stable, and carries nothing when both are stable.
  • Pro -> Swap skips stable, unsupported, or same-as-From targets; cross-network carry switches the account network first and uses that network's native token as FromToken.
  • Keep server keyword-search membership authoritative so localized stock-tag matches are not removed by local Fuse filtering (OK-60609).
  • Align the app token DTO with the server subtitles: string[] contract while retaining the legacy string shape during rollout.

Intent & Context

  • OK-55190: ordinary Swap and Pro are independent exchange surfaces. A token manually selected on one side should be available on the other side once, without creating a ping-pong carry or replacing state after a programmatic Market entry.
  • OK-60609: the server can match stock tokens through localized company-name aliases, but the client previously re-filtered those results by local symbol/name fields and could also lose subtitles while decorating all-network results with balances.

Design Decisions

OK-55190

  • Manual intent is identity-bound rather than boolean-only: Swap stores the exact From/To pair snapshot, while Pro stores the exact manually selected target token.
  • Programmatic token owners clear the corresponding intent before replacement. Swap carry also validates the snapshot against current atoms before consuming it, so stale or replaced state cannot be carried later.
  • Direction alternation keeps an armed Swap snapshot aligned with the visible pair.
  • Stable-coin classification uses one canonical bulk request through usePromiseResult. Timeout or request failure falls back to non-stable for that result only; no foreground module cache persists a failure as false. Focus and reconnect revalidation remain enabled.
  • Pro -> Swap uses a two-phase handoff: prepare captures the exact source token without consuming it; cross-network account update completes first; a Jotai action atomically claims the still-current manual token; the tab then switches and another action applies From/To/amount changes synchronously. This path does not depend on a React rerender or render-derived refs.
  • Market entry paths clear both carry intents before taking ownership of the Pro target.
  • Stock tokens are not carried in either direction. The requirement's NVDA example still needs product confirmation if Stock-tab scope is intended.

OK-60609

  • Server keyword results define list membership. Local Fuse results only provide highlight metadata.
  • All-network balance decoration preserves server subtitles aliases.
  • Search analytics count the server-backed rows returned by the hook rather than the Fuse highlight subset.

Risk Assessment

  • Risk Level: Medium
  • Affected Platforms: iOS/Android for Swap <-> Pro carry; all platforms for token search
  • Remaining gates: native runtime interaction has not been rerun on this head; Stock/NVDA scope needs product confirmation; server deployment and production latency for the subtitle contract were not independently verified here

Test plan

  • Jotai action, Market entry, carry orchestration, stable-classification, and search utility suites: 4 suites / 113 tests passed
  • usePromiseResult focus/reconnect/key lifecycle suite: 32 tests passed
  • yarn agent:check --profile commit
  • git diff --check
  • Desktop dev evidence for localized Apple stock-tag search remains covered by the retained server-authoritative search implementation
  • Native: manually select Swap target, switch to Pro, and verify stable/non-stable/both-stable behavior
  • Native: manually select Pro target, switch to Swap, and verify same-network, cross-network, stable, unsupported, and same-as-From behavior
  • Native: verify plain tab flips and Market programmatic entry do not replay stale carry intent

Issues

  • Fixes OK-55190
  • Fixes OK-60609

@zhaono1 zhaono1 changed the title feat: carry target token between Swap and Pro tabs (OK-55190) feat: Swap token carry and stock-tag search fix (OK-55190, OK-60609) Aug 20, 2026
@zhaono1
zhaono1 marked this pull request as ready for review August 20, 2026 02:36
@zhaono1
zhaono1 enabled auto-merge (squash) August 20, 2026 02:36
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@sidmorizon

Copy link
Copy Markdown
Contributor

@codex review

@sidmorizon

Copy link
Copy Markdown
Contributor

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for security reviews. Please try again later.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

Comment thread packages/kit/src/states/jotai/contexts/swap/actions.ts Outdated
Comment thread packages/kit/src/views/Swap/pages/components/SwapHeaderContainer.tsx Outdated
Comment thread packages/kit/src/views/Swap/pages/components/SwapHeaderContainer.tsx Outdated
Comment thread packages/kit/src/views/Swap/hooks/useSwapProTokenCarry.ts Outdated
@sidmorizon

Copy link
Copy Markdown
Contributor

@cursoragent review

@cursor

cursor Bot commented Aug 20, 2026

Copy link
Copy Markdown

PR #12932 代码审查报告

审查概要

  • 变更范围: 10 个文件, +859 / -24 行
  • 风险等级: High
  • 涉及平台: Mobile(OK-55190 tab 携带);All platforms(OK-60609 搜索)
  • Codex 交叉验证: ⏭️ 未启用
  • PR 评论分析: ✅ 已分析 (12 条评论, 其中 5 条来自 Bot)

评分

维度 得分 说明
🔒 安全性 8/10 无签名/密钥/鉴权改动;陈旧 marker 会静默改账户网络,属正确性而非漏洞
💎 代码质量 5/10 两条 High:marker 置位过宽、消费路径过窄;stable-coin 请求阻塞 tab 切换
🏛️ 架构合理性 7/10 规则抽到 hook 合理,包边界未破坏;生命周期却挂在 UI 分支,旁路切 tab 绕过消费
✅ 完整性 6/10 纯函数单测覆盖 carry 规则;marker 生命周期、错误网络源、冷启动 Limit→Swap 无测试
总分 6.6/10 ⚠️ 需修改后复审

PR 评论分析

来源 类型 发现 判定 说明
@sidmorizon 👤 Human marker 在所有 tab 选币路径置位 ✅ Confirmed {Cross-validated ✅} selectFromToken/selectToToken 无 tab 门闩;Stock 支付币、Pro jump、market jump 都会置位
@sidmorizon 👤 Human Pro marker 只有 LIMIT→SWAP 一条消费路径 ✅ Confirmed {Cross-validated ✅} LIMIT→STOCK、handleJumpToSwapActionuseSwapGlobal 都绕过
@sidmorizon 👤 Human tab 切换被 stable-coin 请求阻塞最长 2s ✅ Confirmed {Cross-validated ✅} await carrySwapTokenToPro()swapTypeSwitchAction 之前;空 contractAddressList 仍会发请求
Devin / @sidmorizon 🤖/👤 Pro 选币稍后注入 Swap 📋 Already Covered 与上一条同一泄漏,复现路径一致
@originalix 👤 Human Limit→Swap 用了错误网络上下文 🔍 Enriched {Cross-validated ✅} useSwapAddressInfo(FROM).networkId 在 native LIMIT 下是 Pro 网络;fromToken ?? accountNetworkId 优先级也反了
chatgpt-codex-connector 🤖 Bot Codex usage limits ❌ Noise 用量提示,无代码发现
@sidmorizon 👤 Human @codex review / @cursoragent review ❌ Noise 触发审查,无独立缺陷

评论误报分析

  • [Codex bot] 误报: 仅用量限制提示,不含可验证的代码问题。

发现的问题

[🔴 高] [🔵 High] Swap marker 在所有 tab 的选币路径上都会被置位 {Cross-validated ✅}

文件: packages/kit/src/states/jotai/contexts/swap/actions.ts:943
类型: 运行时
来源: [@sidmorizon]
描述: 注释写「ordinary Swap 的真实选币」,但 selectFromToken / selectToToken 是共享 action。Stock 改支付币、SwapProActionButton.handleJumpToSwapActionuseSwapGlobal market jump、SwapAlertContainer wrapped 替换、最近交易对都会置位。swapTypeSwitchAction 跨 tab 不清 marker。

复现:Swap → Stock 改支付币 → 回 Swap(STOCK 提前 return,不消费,并 restore swapLastNonLimitSelectedTokens)→ 点 Pro。carrySwapTokenToPro 把 restore 出来的 ToToken 写入 swapProSelectTokenAtomsetSwapProSelectToken 同步写 simpleDb.swapProSelectToken,覆盖持久化,用户在普通 Swap 里可能从未选过币。

Auto-fix:

-      set(swapUserSelectedTokensAtom(), true);
+      if (
+        swapTypeSwitchValue === ESwapTabSwitchType.SWAP ||
+        swapTypeSwitchValue === ESwapTabSwitchType.BRIDGE
+      ) {
+        set(swapUserSelectedTokensAtom(), true);
+      }

selectToToken 需先 get(swapTypeSwitchAtom()) 再套同样门闩。跨 tab 时在 swapTypeSwitchAction 清一次 marker。


[🔴 高] [🔵 High] Pro marker 只在 LIMIT→SWAP 被消费,其它离开 Pro 的路径会残留 {Cross-validated ✅}

文件: packages/kit/src/views/Swap/pages/components/SwapHeaderContainer.tsx:305
类型: 运行时
来源: [@sidmorizon] / [Devin]
描述: swapProUserSelectedTokenAtom 只在 prepareProTokenCarryToSwap 清除,而该方法只挂在 platformEnv.isNative && LIMIT && newType === SWAP

旁路:

  1. LIMIT → STOCK(286 行提前分支)
  2. handleJumpToSwapAction 直接 setSwapTypeSwitch(SWAP)
  3. useSwapGlobalswapTypeSwitchAction

复现:Pro 手选 token → action button 跳 Swap(并写入金额)→ 之后任意一次 Pro→Swap,陈旧 marker 让 apply() 清空金额;跨链时再 updateSelectedAccountNetwork。用户视角是「只是切了 tab」。

修复建议: 在 swapTypeSwitchActionoldType === LIMIT && normalizedType !== LIMIT 时清 Pro marker;prepareProTokenCarryToSwap 改为切换前读快照。handleJumpToSwapAction 至少就地清除。


[🔴 高] [🔵 High] Limit→Swap carry 用 Pro 网络当 Swap 上下文 {Cross-validated ✅}

文件: packages/kit/src/views/Swap/hooks/useSwapProTokenCarry.ts:291
类型: 运行时
来源: [@originalix]
描述: 两层错位叠在一起。

  1. 调用侧传入 useSwapAddressInfo(FROM).networkIduseSwapAccount.tsisNative && tab === LIMITfocusSwapPro = truetokenNetworkId 是 Pro 标的网络,不是账户选择器网络。
  2. 实现是 fromToken?.networkId ?? accountNetworkId,与 PR 写的「用 Swap 账户网络,而不是 FromToken」相反。fromToken 未恢复的冷启动会落到 Pro 网络。

结果:跨链判断和 native-default guard 按错误网络比较;该切账户网络时不切。同一份 networkId 还会传给 swapTypeSwitchAction(..., fromToken?.networkId || networkId)

Auto-fix:

-    const swapContextNetworkId = fromToken?.networkId ?? accountNetworkId;
+    const swapContextNetworkId = accountNetworkId ?? fromToken?.networkId;

调用侧改传 activeAccount.network?.id(all-network 再回退 fromToken?.networkId),并补「native Limit 退出且无 restored Swap pair」集成测试。


[🟡 中] [🔵 High] tab 切换被 stable-coin 请求阻塞最长 2s {Cross-validated ✅}

文件: packages/kit/src/views/Swap/pages/components/SwapHeaderContainer.tsx:293
类型: 性能
来源: [@sidmorizon]
描述: SegmentControl / tab 选中态跟 swapTypeSwitchAtom,只有 swapTypeSwitchAction 之后才翻转。await carrySwapTokenToPro() 插在前面,内部 Promise.race 最长 2000ms。预热 effect 挡不住「刚选完立刻切」——memoizee({ promise: true }) 仍等同一个 in-flight。

fetchSwapStableTokenKeys 对原生币只跳过地址、map.set 仍执行。from/to 都是原生币时会发出 [{ networkId, contractAddressList: [] }]

Auto-fix:

-          await carrySwapTokenToPro();
-        }
-        void swapTypeSwitchAction(newType, networkId);
+          void carrySwapTokenToPro();
+        }
+        void swapTypeSwitchAction(newType, networkId);
   const list = Array.from(
     candidates
       .reduce((map, token) => {
         ...
         map.set(token.networkId, addresses);
         return map;
       }, new Map<string, string[]>())
       .entries(),
-  ).map(([networkId, contractAddressList]) => ({
-    networkId,
-    contractAddressList,
-  }));
+  )
+    .filter(([, contractAddressList]) => contractAddressList.length > 0)
+    .map(([networkId, contractAddressList]) => ({
+      networkId,
+      contractAddressList,
+    }));
+  if (list.length === 0) return new Set<string>();

修改清单

优先级 置信度 文件 类型 描述 Auto-fix
🔴 高 🔵 High actions.ts:943 运行时 Swap marker 未按 tab 收敛,跨 tab 残留
🔴 高 🔵 High SwapHeaderContainer.tsx:305 运行时 Pro marker 只在 LIMIT→SWAP 消费
🔴 高 🔵 High useSwapProTokenCarry.ts:291 运行时 冷启动用 Pro 网络当 Swap 上下文
🟡 中 🔵 High SwapHeaderContainer.tsx:293 性能 切 Pro 被 stable-coin 请求阻塞最长 2s

测试建议

  1. Native:Swap 手选非稳定 ToToken → Pro,应携带为 Pro 标的
  2. ToToken 稳定 / FromToken 非稳定 → 带 FromToken;两边都稳定 → 不带
  3. Pro 标的 → Swap 成为 ToToken;稳定、等于 FromToken、或不支持网络 → 不带
  4. 跨链:BSC Swap 上下文 + Solana JUP → Swap 落到 SOL-JUP 并切网络
  5. 无手选的纯 tab 来回,各自状态保持,不回声
  6. 回归(本审查):Swap → Stock 改支付币 → Swap → Pro,不得覆盖已有 Pro 标的
  7. 回归(本审查):Pro 手选 → action button 跳 Swap 并输入金额 → 再 Pro→Swap,不得清金额、不得切走网络
  8. 回归(本审查):冷启动无 restored Swap pair,从 native Limit 回 Swap,跨链判断应对账户网络而不是 Pro 网络
  9. Token selector 搜「苹果」/「Apple」仍应展示服务端命中的股票代币;字面关键字仍有 symbol 高亮

GH 评论操作

以下问题已在本回合直接发为 inline / reply:

  • Swap marker 过宽置位 — 回复 actions.ts:943
  • Pro marker 消费路径过窄 — 回复 SwapHeaderContainer.tsx:305
  • 2s 阻塞切 tab — 回复 SwapHeaderContainer.tsx:293
  • 错误网络上下文 — 新评论 useSwapProTokenCarry.ts:291

达标问题已在本回合直接发为 inline comments,无需再确认。


OK-60609 一侧(服务端关键字结果作为匹配权威、fuse 只做高亮、balance merge 保留 subtitles)与题面一致,未发现新的过滤回退。合入前应先修上面三条 High:marker 生命周期收进 swapTypeSwitchAction,并把 Swap 上下文网络从账户选择器取,而不是 Pro-focused 的 useSwapAddressInfo

Open in Web Open in Cursor 

Comment thread packages/kit/src/views/Swap/hooks/useSwapProTokenCarry.ts Outdated
Comment thread packages/kit/src/views/Swap/hooks/useSwapProTokenCarry.ts Outdated
Comment thread packages/kit/src/states/jotai/contexts/swap/actions.ts Outdated

@originalix originalix left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@zhaono1

Automated code review found blocking issues

Reviewed commit ca96d0bcdb7b.

  • P1 · Tab switching clears pending carry markers during network cold start

View the full review report

Comment thread packages/kit/src/views/Swap/hooks/useSwapProTokenCarry.ts Outdated
Comment thread packages/kit/src/views/Swap/pages/components/SwapHeaderContainer.tsx Outdated
Comment thread packages/kit/src/views/Swap/hooks/useSwapProTokenCarry.ts Outdated

@originalix originalix left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@zhaono1

Automated code review found blocking issues

Reviewed commit 9f6a7d4c543c.

  • P1 · Tab switching clears pending carry markers during network cold start

View the full review report

@originalix originalix left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@zhaono1

Automated code review found blocking issues

Reviewed commit fdd3a88686e5.

  • P2 · Market entry reuses a stale Pro carry marker

View the full review report

Comment thread packages/kit/src/views/Swap/hooks/useSwapProTokenCarry.ts Outdated
Comment thread packages/kit/src/views/Swap/hooks/useSwapProTokenCarry.ts Outdated
Comment thread packages/kit/src/views/Swap/hooks/useSwapPro.ts Outdated
Comment thread packages/kit/src/views/Swap/hooks/useSwapProTokenCarry.ts Outdated
Comment thread packages/kit/src/views/Swap/pages/components/SwapHeaderContainer.tsx Outdated
Comment on lines +325 to +332
if (
!isAllNetworkSelected &&
proCarry?.targetNetworkId &&
proCarry.targetNetworkId !== selectedAccountNetworkId
) {
await updateSelectedAccountNetworkAction(proCarry.targetNetworkId);
} else if (
!isAllNetworkSelected &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 P1 新增的「跳过冗余切换」让主路径掉进 else if,把账户网络切到被丢弃的旧链

问题

本次新增的 proCarry.targetNetworkId !== selectedAccountNetworkIdresolveSwapContextNetworkId 里新写的注释("Pro keeps the account selector aligned with its own target network while active")互相矛盾。useSwapProAccountuseSwapPro.ts:342-366)在 Pro 活跃时会把账户网络同步成 swapProSelectToken.networkId;而 claimProTokenCarryactions.ts:805)要求 swapProUserSelectedToken === swapProSelectToken === sourceProToken 才返回 true,所以能成功 carry 的 proCarry.targetNetworkId 恒等于 selectedAccountNetworkId。新条件因此在真实 native Pro 退出路径上恒为 false,第一分支变成死代码。

问题在于这是 if / else if 链:第一分支被跳过后立刻进入第二分支 fromToken.networkId !== selectedAccountNetworkId,而此时 fromToken 正是要被丢弃的普通 Swap 旧链 token(swapTypeSwitchAction 在 native 进 LIMIT 时于 actions.ts:3577 提前 return,from/to atom 原样保留)。于是本该「保持在目标网络」的场景,反而 await updateSelectedAccountNetworkAction(fromToken.networkId) 切到了旧链。

影响

跨链 carry 主场景(账户已同步到 sol--101,普通 Swap FromToken 在 evm--1,Pro 标的 sol--101):账户网络被切到 evm--1swapTypeSwitchAction(SWAP, 'sol--101') 恢复 lastNonLimit 的 evm--1 组合 → apply() 再写成 from = SOL native / to = JUP。最终 token 对是对的,但账户选择器停在 evm--1,与交易对不一致。

更糟的是这次切换发生在 swapTypeSwitchAtom 仍为 LIMIT 时,useSwapProAccount 的同步 effect 会检测到 currentNetworkId('evm--1') !== targetNetworkId('sol--101') 而反向切回,形成 ping-pong;最终落在哪个网络取决于 Pro 组件卸载与该 effect 的先后,是不确定的。此外这个多余的 await 还拉长了 proCarry.claim() 之前的窗口,正是代码注释里担心的「account-network update in flight」竞态。

本轮新增的单测只断言 hook 侧 plan.targetNetworkId === 'sol--101',而 SwapHeaderContainer 没有任何测试,恰好把「hook 产出正确、消费端丢弃」这条缝隙漏掉了。

建议

不要用 else if 承接被跳过的第一分支,先算出唯一的目标网络再统一去重:

const nextAccountNetworkId =
  proCarry?.targetNetworkId ?? fromToken?.networkId;
if (
  !isAllNetworkSelected &&
  nextAccountNetworkId &&
  nextAccountNetworkId !== selectedAccountNetworkId
) {
  await updateSelectedAccountNetworkAction(nextAccountNetworkId);
}

顺带:swapContextNetworkId 改成 FromToken-first 之后,第 346 行的 fromToken?.networkId || swapContextNetworkId || networkIdswapContextNetworkId 已经只可能在 fromToken 缺失时生效并等于 selectedAccountNetworkId,这段回退链可以一并收敛。建议补一个覆盖 handleSwapTypeChange 分支的用例,否则消费端的回归还会重复出现。

Claude session ↗


Generated by Claude Code

@originalix

Copy link
Copy Markdown
Collaborator

Automated code review found blocking issues

@zhaono1

Reviewed commit 9246066ad5ba. This report always reflects the latest reviewed changes.

Review summary

The native Swap state boundary now records one-shot manual token selections so Swap and Swap Pro can transfer compatible tokens across tab changes, including cross-network native-token setup and amount resets. The change also makes server keyword results authoritative while preserving alias arrays and attaching local Fuse metadata only for highlighting. The carry flow stays within the existing kit/shared boundaries and does not touch signing or secret-handling paths, but stable-token classification must preserve address casing on case-sensitive networks before this behavior is safe to merge.

What needs attention: Verify that stable-token classification uses network-aware address normalization, especially for Solana, Tron, Aptos, Sui, and other case-sensitive networks.

Issues to address

  • P2 · Stable-token prefetch corrupts case-sensitive addresses
    • Impact: buildStableTokenRequestList lowercases every contract address before calling checkStableCoinsList. On Solana, Tron, Aptos, Sui, and other case-sensitive networks this changes the token identity, so the service can return no stable match and the explicit empty-set fallback then incorrectly carries a stable token into Swap Pro.
    • Suggested action: Use normalizeTokenContractAddress for both request construction and stable-token keys so only case-insensitive networks are lowercased.

Validation gaps

git diff --check and TypeScript syntax transpilation completed successfully. Focused Jest tests, lint, and package type checking could not run because the checkout does not contain the required Yarn node_modules state; CI or a fully installed workspace should rerun them after the address-normalization fix.

@zhaono1

zhaono1 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #12961.

@zhaono1 zhaono1 closed this Aug 24, 2026
auto-merge was automatically disabled August 24, 2026 07:57

Pull request was closed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants