涉及 commit:
7a8f537d(feat: Add Permission&Dapp) +e25fe09d(refactor: chain-kit) 总计 431 文件变动,+15,413 / -10,1282 行
一、chain-kit 重构(核心变动)
目的
将原来分散在三个包(crypto、vault、utils/rpc)中的区块链操作统一收敛到 packages/chain-kit 一个包,从 class-based OOP 转向 function-based 模块化设计。同时将 service 层中不涉及状态管理的链操作(如 Service/Nonce)也下沉到 chain-kit,service 只负责业务编排。
前后对比
| 维度 | 旧架构 | 新架构 |
|---|---|---|
| 包结构 | crypto(签名) + vault(keyring管理) + utils/rpc(RPC调用) |
统一 chain-kit |
| 范式 | class 继承链:BaseCrypto → EvmCrypto,BaseVault → EvmVault |
纯函数模块:evm.signTransaction()、sol.broadcastTx() |
| 签名接口 | vault.signTransaction() 需先实例化 Vault |
createSigner(params) 返回 SignerInstance 对象 |
| RPC 接口 | new EvmRpc(url) 类实例 |
evm.getBalance(config, addr) 纯函数 + 内部 client 缓存 |
| Keyring | 独立 HDKeyring/PKKeyring/WatchKeyring 类 |
集成到 SignerInstance.init(secret) 内部 |
| 私钥生命周期 | 无管理,私钥驻留内存直到 GC | 签名后自动销毁,引用计数支持批量场景 |
| Tree-shaking | 差(class 整体引入) | 好(按函数引入) |
新 chain-kit 模块结构
packages/chain-kit/src/
├── signer.ts # SignerInstance 抽象 + createSigner 工厂 + 私钥自动销毁
├── chains/
│ ├── evm/
│ │ ├── signer.ts # EVM 签名(viem)
│ │ ├── rpc.ts # EVM RPC(balance/gas/broadcast/ENS)
│ │ ├── tx.ts # 构建 unsigned tx
│ │ ├── fee.ts # gas 估算
│ │ ├── serialize.ts # tx 序列化
│ │ └── contract/ # ERC20/721/1155 ABI helpers
│ └── sol/
│ ├── signer.ts # Solana 签名(OKXWeb3 SDK)
│ ├── rpc.ts # Solana RPC
│ ├── tx.ts # 构建 transfer tx
│ └── serialize.ts # tx 序列化
└── index.ts # 统一导出
删除的包/模块
packages/vault— 整包删除,keyring 逻辑内化到 SignerInstancepackages/crypto— 重命名为 chain-kit,class 全部改为函数packages/utils/src/rpc/— RPC 类移入 chain-kitpackages/service/src/Nonce/— 整个 Nonce Service 删除
SignerInstance 私钥自动销毁机制
旧架构中 Vault 实例的私钥在签名后无清理,明文驻留 JS 堆内存直到 GC 回收(时机不确定,且不保证内存清零)。新架构在 SignerInstance 内部实现了自动销毁:
核心设计:Uint8Array 安全存储 + 引用计数 + sign 后自动 destroy
privateKey/mnemonic在SignerInstance内部以Uint8Arraybuffer 存储,对外通过 getter/setter 暴露 string 接口(兼容getFullInfoFromVault等消费方)destroy()调用Uint8Array.fill(0)原地清零 buffer 内容,而非仅解除引用等 GC 回收。JS 字符串是 immutable 无法原地清零,Uint8Array可以signTransaction()/signMessage()内部用try/finally包裹,执行完毕后自动调destroy()清零 bufferacquire()/release()引用计数:acquire递增 holds 阻止自动销毁,release递减并在归零时触发销毁,用于signAllTransactions等批量签名场景use(fn)便捷方法:自动acquire→ 执行回调 →release,保证异常路径也能清理destroy()可手动调用,用于导入/派生等不走签名方法的场景- 底层签名库(viem、OKX SDK)仍接受 string,签名时从 buffer 临时解码为 string 传入,签名完成后 buffer 立即清零。临时 string 由 GC 回收,但驻留窗口极短(仅签名函数执行期间)
各场景覆盖:
| 场景 | 路径 | 销毁方式 |
|---|---|---|
| DApp 签名(EVM) | eip155.ts → getVault → signMessage/signTransaction |
sign 方法 finally 自动销毁 |
| DApp 签名(Solana 单笔) | solana.ts → getVault → signTransaction/signMessage |
sign 方法 finally 自动销毁 |
| DApp 批量签名(Solana) | solana.ts → getVault → acquire → N × signTransaction → release |
release 归零后销毁 |
| Trade/Send 转账 | Tx/action.ts → getVault → signTransaction |
sign 方法 finally 自动销毁 |
| 导入/派生钱包 | Wallet/action.ts → importMnemonic/deriveHD/importPrivateKey |
getFullInfoFromVault 后手动 destroy() |
| 导出私钥/助记词到页面 | exportMnemonic/exportPrivateKey 直接解密返回字符串 |
与 signer 无关,由组件 state 管理 |
优劣势
优势:
- API 更简洁,无需管理类实例生命周期
- 纯函数易于测试和 mock
- 减少抽象层级(3包 → 1包),降低心智负担
- 更好的 tree-shaking,按需引入链相关函数
劣势:
- SignerInstance 仍然是有状态对象,不是完全无状态,但通过 Uint8Array 安全存储 + 自动销毁机制将私钥驻留时间压缩到签名操作期间,且 destroy 时原地清零内存
- 底层签名库(viem、OKX SDK)仅接受 string 参数,签名瞬间仍会产生临时 string(由 GC 回收,无法原地清零),这是 JS 运行时的固有限制
- 内部 client 缓存(module-level Map)在 SSR/多实例场景可能有隐患
其他钱包参考
- MetaMask:使用
@metamask/keyring-controller+@metamask/transaction-controller,class-based,但通过 controller 模式解耦 - Rainbow:使用 viem/wagmi 生态,函数式风格,chain-kit 的方向与之类似
- Trust Wallet:wallet-core 用 C++ 实现底层签名,上层 binding 是函数式的
chain-kit 类型规范
遵循「推导 > 定义」原则,chain-kit 内部类型约定:
- 不加 T/I 前缀:
EstimateFeeParams(非IEstimateFeeParams)、SPLToken(非ISPLToken)、SignerInstance(非TSigner) - 函数返回值不显式标注:依赖 TS 推导,仅在推导结果不符合预期时才标注(如
getTxStatus返回TTxStatus来自外部类型包) - 类型就近定义:类型定义在使用它的文件中,不单独放
types.ts - 接口用于公共 API 边界:
SignerInstance、EvmRpcConfig、SolRpcConfig等对外接口保留interface定义
二、Service 双进程架构(Frontend/Backend Bridge)
目的
为 Electron 桌面端实现 main process(后端)和 renderer process(前端)的进程隔离。Service 全量运行在 main process,renderer 通过 IPC bridge 访问。
前后对比
| 维度 | 旧架构 | 新架构 |
|---|---|---|
| 进程模型 | 单进程,Service 直接在 renderer 运行 | 双进程:main process 持有 Service,renderer 持有 ProxyService |
| IPC | packages/channel 通用 event-bus |
专用 transport 层(4 个 IPC channel) |
| 状态同步 | 无(同进程直接访问) | Immer patch 增量同步 + 全量 fallback |
| 安全性 | renderer 可直接访问 vault/密钥 | renderer 只能通过 action 调用,敏感字段被过滤 |
架构图
┌─ Main Process ──────────────────────────────────┐
│ Service (13 stores + effects + RPC Engine) │
│ ↕ onPatch callback │
│ ElectronBackendChannel │
│ - service:action (req/res) │
│ - service:sync (req/res) │
│ - service:patches (broadcast) │
│ - service:event (broadcast) │
└─────────────────────────────────────────────────┘
↕ Electron IPC
┌─ Renderer Process ──────────────────────────────┐
│ ElectronFrontendChannel │
│ ↕ │
│ ProxyService (13 proxy stores) │
│ - 只读 state + async action wrappers │
│ - patch 增量更新 state │
│ - 失败时 full sync fallback │
└─────────────────────────────────────────────────┘
关键实现
- ProxyFactory:为每个 store 创建 Zustand proxy store,action 变为
async (...args) => channel.callAction(storeName, actionName, args) - Patch 同步:backend 每次 state 变更产生 Immer patches → 过滤敏感字段 → broadcast 到 renderer → renderer apply patches
- Fallback:patch apply 失败时自动 requestFullSync 重建状态
- 敏感数据过滤:
filterSensitivePatches()在 broadcast 前剥离密钥等字段
删除的包
packages/channel— 整包删除,被internal/transport.ts+internal/bridge/替代
优劣势
优势:
- 安全性大幅提升,renderer 无法直接接触密钥
- Immer patch 增量同步效率高
- 架构清晰,backend/frontend 职责分明
- 为未来多窗口/多 webview 场景打基础
劣势:
- 所有 action 变为异步
- patch 同步有失败风险,需要 fallback 机制
- 调试复杂度增加(跨进程)
其他钱包参考
- MetaMask:Extension 架构天然双进程(background service worker + popup),使用
@metamask/controllers+ JSON-RPC stream 通信 - Brave Wallet:C++ backend + renderer bridge,类似思路
- Rabby:也是 background + popup 双进程,通过 chrome.runtime.sendMessage 通信
三、DApp 弹框 + Approval/Connector 机制
目的
重建 DApp 连接和审批系统:用 Connector(持久化连接管理)+ ApprovalQueue(临时审批队列)替代旧的 Session + Approval。同时新建完整的 DApp Confirmation UI。
前后对比
| 维度 | 旧架构 | 新架构 |
|---|---|---|
| 连接管理 | Session service(per-origin, per-namespace) |
Connector service(per-origin, per-CAIP-2 chain) |
| 审批模型 | Approval service(持久化,status-based) |
ApprovalQueue(临时,Promise-based) |
| 权限检查 | hasPermission() 查 Session.walletMap |
isConnected() 查 Connector.connections |
| 链标识 | namespace(evm/sol) | CAIP-2(eip155:1, solana:mainnet) |
| UI | ConfirmationProvider + 空壳 DApp Popup |
DAppManager + BlurModal + 完整 Confirmation 组件树 |
| 审批持久化 | 是(Approval 存储完整 request/response) | 否(ApprovalQueue 仅内存,Promise resolve 后消失) |
新 Connector 模型
Connection {
origin: string // DApp 域名
caip2: Caip2ChainId // "eip155:1" | "solana:mainnet"
accounts: AccountRef[] // 授权的钱包/账户
siteMetaData: { name, icon }
grantedAt / updatedAt / lastSelectedAt / expiresAt
}
核心 action:connect() / disconnect() / addAccount() / removeAccount() / sweepInvalid() / sweepExpired()
新 ApprovalQueue 模型
PendingApproval {
id: string
origin: string
caip2: Caip2ChainId
method: string // "eth_sendTransaction" 等
data: unknown
createdAt: number
}
核心 action:requestApproval() → 返回 Promise / approve(id, result) / reject(id, error) / rejectAll()
端到端流程(以 eth_sendTransaction 为例)
DApp 调用 provider.send('eth_sendTransaction', [...])
→ RPC Engine 检查 isConnected(origin, caip2, address)
→ approval handler 调用 approvalQueue.requestApproval({...})
→ DAppManager 检测到 pending approval → 弹出 BlurModal
→ 用户确认 → approvalQueue.approve(id, result)
→ Promise resolve → handler 签名 + 广播 → 返回 txHash
DApp Confirmation UI 结构
packages/view/src/DApp/
├── index.tsx # DAppManager(监听 approvalQueue)
└── Confirmation/
├── index.tsx # 按 method 路由
├── Layout.tsx # 通用确认布局(app info + 按钮)
├── Connect/ # eth_requestAccounts
├── SwitchNetwork/ # wallet_switchEthereumChain
├── AddNetwork/ # wallet_addEthereumChain
├── WatchAsset/ # wallet_watchAsset
└── chains/
├── Evm/
│ ├── SignMessage/ # personal_sign, eth_signTypedData
│ └── SignTransaction/ # eth_sendTransaction
└── Sol/ # solana_signTransaction 等
删除的模块
packages/service/src/Session/— 整个 Session servicepackages/service/src/Approval/— 旧 Approval servicepackages/router/src/providers/ConfirmationProvider.tsx— 旧确认弹框 providerpackages/view/src/Trade/DApp/— 旧 DApp 空壳组件packages/rpc-engine/src/sessionScopes.ts— 旧 session scope 逻辑
优劣势
优势:
- CAIP-2 标准化链标识,扩展性更好
- Promise-based 审批更简洁,无需持久化中间状态
- Connector 与 ApprovalQueue 职责分离清晰
- 完整的 Confirmation UI 覆盖所有 DApp 交互场景
劣势:
- ApprovalQueue 不持久化意味着进程崩溃会丢失 pending 请求
其他钱包参考
- MetaMask:使用
@metamask/permission-controller实现 EIP-2255 权限模型,ApprovalController管理审批队列,也是 Promise-based - Phantom:Solana 原生钱包,连接模型类似但更简单(无多链)
- Rainbow:使用 WalletConnect 协议管理 DApp 连接,session 模型不同
四、Service 内部 action/selector 职责分离
目的
将所有不涉及 state 变更的逻辑从 action 移到 selector,确立 action = 写、selector = 读 的清晰边界。
前后对比
| 维度 | 旧架构 | 新架构 |
|---|---|---|
| action | 混合:state 变更 + 查询 + 派生计算 | 纯写入:set*、add*、remove*、update*、fetch* |
| selector | 简单 getter | 承担所有读操作:lookup、filter、search、computed、count |
具体迁移示例
- Token:
getTokenById()、getTokenByContract()、searchToken()、nativeTokens、tokenCount→ 全部移入 selectors - Tx:
txsByNetwork()、pendingTxs、recentTxs(limit)、txCount、hasTx→ selectors - Security:
isUrlBlacklisted()(含 URL 解析逻辑)、allBlackListUrls、blackListCount→ selectors - Wallet:
getWallet()、getAccount()、walletCount、totalAccountCount→ selectors - Currency:
fiats、usdRate、cnyRate、hasCurrencyData→ selectors
例外
有副作用的函数(如 exportMnemonic()、getVault() 涉及解密)仍留在 action。
意义
- selector 是纯函数,可安全在 renderer 侧本地执行,不需要走 IPC
- 与双进程架构配合:action 走 bridge 到 main process,selector 在 proxy store 本地计算
- 更容易做 memoization 和性能优化
五、类型系统:推导 > 定义
目的
用 Zod schema 作为类型的 single source of truth,通过 z.infer<typeof Schema> 推导类型,逐步淘汰 packages/types/ 中手写的 interface。
前后对比
| 维度 | 旧架构 | 新架构 |
|---|---|---|
| 类型来源 | packages/types/ 手写 interface |
service schema z.infer<> 推导 |
| 交易类型 | 6 个独立 interface(TransferNative/Token/NFT × EVM/Sol) | 2 个合并 interface(TransferTx × EVM/Sol),optional token/NFT 字段 |
| 验证 | 类型和运行时验证分离 | Zod schema 同时提供类型 + 运行时验证 |
具体变化
Tx 类型合并:
旧:IEvmTransferNativeTx / IEvmTransferTokenTx / IEvmTransferNFTTx / IEvmStakingTx
新:IEvmTransferTx { ..., token?: {...}, NFT?: {...} }
Service schema 推导模式:
// Connector/schema.ts
export const ConnectionSchema = z.object({ origin: z.string(), caip2: ..., accounts: ... })
export type Connection = z.infer<typeof ConnectionSchema>
// Tx/schema.ts
export const TxSchema = z.object({ id: z.string(), type: z.enum([...]), status: z.enum([...]), ... })
export type Tx = z.infer<typeof TxSchema>
已删除的类型文件
packages/types/src/dapp.ts— DApp 类型(被 Connector schema 替代)packages/types/src/message.ts— 消息类型
仍保留在 types/ 的(后续会逐步迁移)
address、api、auth、currency、fee、history、NFT、network、token、wallet 等 — 等各自 service schema 完善后迁入
六、其他重要变动
6.1 Send 流程合并
packages/view/src/Trade/SendNew/ 改为 packages/view/src/Trade/Send/。
6.2 Transfer Service 合并进 Tx
packages/service/src/Transfer/ 整个删除,逻辑合并到 Tx/action.ts。
SendTransactionTx类型支持transfer(转账)和dapp(合约交互)两种- 统一
sendTransaction()入口
6.3 Nonce Service 删除
变动: 整个 packages/service/src/Nonce/ 目录删除,nonce 获取逻辑内联到 Tx/action.ts,直接调用 evm.getNonce()。
原因: 旧实现在广播成功后本地 incrementNonce(),但 fetchNonce() 每次都走 RPC 从链上拿,本地值从未被读取 — incrementNonce 是死代码,给人一种“并发安全”的错觉但实际不起作用。
修复方式:
rpc.ts的getNonce加blockTag: 'pending',确保拿到包含 mempool 中 pending 交易的 nonce- 删除广播后的
incrementNonce调用 - 删除整个 Nonce Service(schema/action/selectors/effects),从
core.ts、frontend.ts、backend.ts中移除所有引用 Tx/action.ts中fetchNonce改为内联函数,直接调evm.getNonce
为什么不需要本地 nonce:
- MegaETH(主要服务对象):~10ms mini block 出块,交易几乎瞬间确认,
getTransactionCount返回的就是最新值 - 标准 EVM 链:
blockTag: 'pending'会返回包含 mempool 交易的 nonce,覆盖绝大多数场景 - 钱包 UI 不会触发毫秒级连续发送,无需 mutex
其他钱包对比:
| 钱包 | Nonce 策略 | 本地状态 |
|---|---|---|
| MetaMask | @metamask/nonce-tracker + per-(chainId, address) mutex 锁,考虑 pending tx 列表 |
有,但通过 mutex 保证一致性 |
| Rainbow | 同时拿 pending + latest 两个 count,对比本地状态,处理 private mempool 超时 |
有,用于 private mempool 场景 |
| Mega Wallet | getTransactionCount('pending') 每次从链上拿,无本地状态 |
无 |
MetaMask/Rainbow 维护本地 nonce 是因为它们面对的是 12s+ 出块的标准 EVM 链 + private mempool 场景。MegaETH 10ms 出块 + 无 private mempool,链上 nonce 始终是最新的,本地维护纯属多余。
6.3 DApp 多标签浏览
新增 packages/store/src/apps.ts(Zustand store)管理多 DApp 标签页:
openedDapps/activeDappId状态DappTabs组件:标签栏 UIDappWebViewLayer:多 WebView 实例管理,仅 active 的可交互Apps页面:DApp 目录浏览(Bookmark/Featured/DeFi/Games 等分类)
6.4 新增 Hooks
useCurrent():便捷获取当前 walletId/accountId/networkId/addressuseEstimateEvmFee:实时 EVM gas 估算 hook
七、影响范围总览
| 包 | 变动级别 | 说明 |
|---|---|---|
chain-kit(新) |
🔴 全新 | 替代 crypto + vault + utils/rpc |
crypto |
🔴 删除 | 被 chain-kit 替代 |
vault |
🔴 删除 | 被 chain-kit/signer 替代 |
channel |
🔴 删除 | 被 service/internal/transport 替代 |
service |
🔴 大改 | 新增 internal/bridge、Connector、ApprovalQueue;删除 Session、Approval、Transfer、Nonce、Notifaction |
rpc-engine |
🟡 中改 | 适配新 Connector 权限模型,删除 sessionScopes |
view |
🟡 中改 | 新增 DApp/Confirmation 完整 UI;Send 流程合并;删除旧 DApp 空壳 |
hooks |
🟡 中改 | useService hooks 适配双进程;新增 useCurrent、useEstimateEvmFee |
router |
🟢 小改 | 删除 ConfirmationProvider |
store |
🟢 小改 | 新增 apps store |
types |
🟢 小改 | tx 类型重构,删除 dapp/message 类型 |
utils |
🟢 小改 | 删除 rpc/、event.ts、global.ts 部分导出 |
provider |
🟢 小改 | 适配 chain-kit import |
components |
🟢 小改 | BlurModal、NetworkList 小调整 |
ui |
🟢 小改 | WebView 组件适配 |
desktop |
🟡 中改 | main.ts 适配双进程 Service 初始化 |
constants |
🟢 小改 | 删除 event-bus 常量 |