Skip to main content

难点:过度面向接口

补充难点: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 只覆盖了转账的「表面字段」,各链真正的交易模型差异很大:

核心差异参数统一接口的代价
EVMnonce, gasLimit, gasPrice / EIP-1559, data(合约调用)要把 gas 模型硬塞进通用 params
Bitcoinutxos[], byteFee, changeAddress, hashType根本没有 account nonce,是 UTXO 模型
SolanarecentBlockhash, feePayer, instructions[], isToken2022账户模型 + 指令数组,和 EVM 完全不同
Tron (TVM)energy, bandwidth, 资源不足时的 shortageType费用模型是资源消耗,不是 gas
Cosmosmemo, 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?: { ... }[];
}

强行统一接口会导致:

  1. 泛型爆炸ManageService 要 6~7 个类型参数,读代码成本高
  2. 假共性BaseBuildParams 看起来通用,各链实现时大量 optional / 扩展字段
  3. 抽象泄漏 — 上层 UI 最终还是要 if (chain === 'bitcoin') 或直接用链专属 Service
  4. 改一条链牵动全局 — 加 Cardano 的 Byron/Shelley 地址逻辑,要动「通用接口」
  5. 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.tsxBaseEvmService
  • bitcoin-send-confirm.tsxBaseBitcoinService
  • solana-send-confirm.tsxBaseSvmService

MPC 签名适配器同样按链拆分(evm-mpc-send-adapter.tsbitcoin-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(BaseEvmServiceBaseBitcoinService 等),仅共享结果类型和工具函数;UI 和 MPC 适配器按链调用,不再强制实现统一接口。