难点:过度面向接口
补充难点:src/services/transaction 过度面向接口
这是多链钱包里很典型的一类坑:想用一个统一抽象覆盖所有链,结果抽象层比业务还复杂,每条链还要不断「破例」。
最初的思路
一开始在 src/services/transaction 里做了面向接口编程,核心是:
// 定义基础交易构建参数类型
export interface BaseBuildParams {
// 其他公共参数,例如 from 地址、nonce、gasLimit 等
from: string;
to: string;
amount: string;
chainId: string;
}
再往上套一个泛型 ManageService,期望所有链都实现同一套方法:
export interface ManageService<
TBuildParams extends BaseBuildParams,
TBroadcastParams extends BaseBroadcastParams,
TBuildOutput,
TBroadcastOutput,
TSignParams,
TSignOutput,
TFeeEstimate = CommonFeeEstimate,
> {
buildTransaction(params: TBuildParams): Promise<BuildResult<TBuildOutput, TFeeEstimate>>;
broadcastTransaction(params: TBroadcastParams): Promise<BroadcastResult<TBroadcastOutput>>;
signTransaction(params: TSignParams, activeWalletId: string): Promise<SignResult<TSignOutput>>;
getNonce?(address: string): Promise<number>;
getNativeBalance(address: string, decimals?: number): Promise<string>;
getTokenBalance(address: string, tokenAddress: string, decimals?: number): Promise<string>;
}
想法很「正统」:统一 build → sign → broadcast,上层 UI 只依赖接口。
实际问题:各链模型根本不一样
from / to / amount / chainId 只覆盖了转账的「表面字段」,各链真正的交易模型差异很大:
| 链 | 核心差异参数 | 统一接口的代价 |
|---|---|---|
| EVM | nonce, gasLimit, gasPrice / EIP-1559, data(合约调用) | 要把 gas 模型硬塞进通用 params |
| Bitcoin | utxos[], byteFee, changeAddress, hashType | 根本没有 account nonce,是 UTXO 模型 |
| Solana | recentBlockhash, feePayer, instructions[], isToken2022 | 账户模型 + 指令数组,和 EVM 完全不同 |
| Tron (TVM) | energy, bandwidth, 资源不足时的 shortageType | 费用模型是资源消耗,不是 gas |
| Cosmos | memo, gasUsed, amino/protobuf 编码 | 和 EVM 签名流程也不同 |
实际代码里,每条链都定义了自己的 params,和 BaseBuildParams 已经差很远:
export interface BitcoinBuildParams {
utxos: BitcoinUtxo[];
changeAddress?: string;
byteFee?: number;
hashType?: number;
amount: number;
from: string;
to: string;
chainId: string;
}
export interface EvmSignParams {
chainId: string;
from: string;
to: string;
amount: string;
nonce?: number;
gasLimit?: string;
data?: string;
decimals?: number;
maxFeePerGas?: string;
maxPriorityFeePerGas?: string;
gasPrice?: string;
}
export interface SvmBuildParams {
from: string;
to: string;
amount: string;
chainId: string;
recentBlockhash?: string;
feePayer?: string;
tokenAddress?: string;
decimals?: number;
isToken2022?: boolean;
instructions?: { ... }[];
}
强行统一接口会导致:
- 泛型爆炸 —
ManageService要 6~7 个类型参数,读代码成本高 - 假共性 —
BaseBuildParams看起来通用,各链实现时大量 optional / 扩展字段 - 抽象泄漏 — 上层 UI 最终还是要
if (chain === 'bitcoin')或直接用链专属 Service - 改一条链牵动全局 — 加 Cardano 的 Byron/Shelley 地址逻辑,要动「通用接口」
- AI 协作更差 — 改 Bitcoin UTXO 逻辑却要先理解整套泛型抽象
目前代码里 ManageService 没有任何 Service 真正 implements,说明这套抽象在实践中已经被绕开了。
调整方向:按链拆分,只共享真正通用的部分
现在的结构更务实:
src/services/transaction/
├── types.ts # 只保留通用结果类型、费用 union、状态枚举
├── BaseEvmService.ts # EVM 专属 params + 逻辑
├── BaseBitcoinService.ts # UTXO 专属
├── BaseSvmService.ts # Solana 专属
├── BaseTvmService.ts # Tron 专属
├── BaseCosmosService.ts
├── SuiService.ts / AptosService.ts / ...
└── util.ts # 真正可复用的工具函数
保留的共性(值得抽象):
BuildResult/BroadcastResult/SignResult— 流程结果结构类似TransactionStatus— 状态枚举CommonFeeEstimate— 费用展示的 union type
不再强制的共性(各链自己定义):
- build / sign 的入参
- 费用计算逻辑
- 广播、查 receipt 的实现
UI 层也按链直接用具体 Service,而不是统一接口:
ethereum-send-confirm.tsx→BaseEvmServicebitcoin-send-confirm.tsx→BaseBitcoinServicesolana-send-confirm.tsx→BaseSvmService
MPC 签名适配器同样按链拆分(evm-mpc-send-adapter.ts、bitcoin-mpc-send-adapter.ts),而不是一个 MpcTransactionAdapter<T>。
和 Native 层重构是同一条思路
| 层级 | 过度设计 | 务实做法 |
|---|---|---|
| iOS Services | 单文件塞所有链 | 按链拆 XxxTransactionService + XxxTypes |
| TS transaction | 统一 ManageService 接口 | 按链拆 BaseXxxService,各管各的 params |
核心原则一致:多链钱包的差异在「交易模型」,不在「方法名」。方法名可以都叫 buildTransaction,但参数和内部逻辑差太多,统一 interface 的收益小于维护成本。
可写进文档的一句话
难点:早期对
src/services/transaction做面向接口编程,用ManageService+BaseBuildParams试图统一各链 build/sign/broadcast;但 Bitcoin(UTXO)、EVM(nonce/gas)、Solana(blockhash/instructions)等链的参数模型差异极大,导致泛型复杂、抽象泄漏、扩展困难。
方案:改为按链独立 Service(BaseEvmService、BaseBitcoinService等),仅共享结果类型和工具函数;UI 和 MPC 适配器按链调用,不再强制实现统一接口。