鸿蒙适配Flutter链上交互库on_chain:私钥签名与多链资产治理实践 把 on_chain 这个 Flutter 三方库迁到鸿蒙生态我验证了一个不太直观的结论区块链链上交互的适配难点根本不在 Dart 代码本身而在于你愿不愿意把平台差异提前暴露在架构层。这篇文章是我在内部项目里把高阶链上交互、高性能私钥签名和多链资产治理完整搬到鸿蒙系统的全记录。没有通篇理论更多是操作细节和踩坑记录希望给准备走同样路的同行省点时间。先交代背景。我们的产品需要在鸿蒙设备上提供一个“不可篡改”的数字化底座用户手里有若干私钥要对不同链上的资产做统一视图和链上操作。最初想自己写协议层后来发现每个链都自己维护一套地址、交易、签名逻辑工作量完全失控。于是换成了 on_chain 这类现成的 Flutter 库但真正上手才发现“能用”和“能在鸿蒙上跑起来”之间隔着一大堆边界问题。1. 为什么偏偏是 on_chain鸿蒙上的 Dart 生态与链上交互的缘分选 on_chain 之前我对比过好几个方案。有些库用起来很爽但内部塞了一堆原生加密代码在鸿蒙的 Flutter 引擎下根本没法编译有些库为了支持几十条链把 RPC 节点配置写死在代码里想换一个安可环境的自建节点都费劲。on_chain 给我的第一印象是“干净”——它把地址生成、交易序列化、消息哈希、签名验证这些核心逻辑尽量用纯 Dart 实现天然对平台依赖极低。这个特性在鸿蒙适配里几乎是决定性的。鸿蒙的 Flutter 生态目前还没有完全跟上主流 Flutter 的插件版本很多依赖原生 Android/iOS 实现的包到了鸿蒙就直接断掉。on_chain 如果能把大部分逻辑留在 Dart 层鸿蒙侧的核心工作就会从“重写加密逻辑”变成“对接平台通道和系统能力”难度降了一个数量级。另一个让我坚定用它做底座的原因是它暴露了类似协议适配器的结构。每个链的地址格式、交易字段、签名算法都能以适配器形式注入而不是把所有链揉在一个大 switch 里。对我这种要做多链资产治理的团队来说这个设计简直是对症下药我在鸿蒙侧的适配成本是单次投入后面每加一条链只需要写一个新的适配器不需要动核心签名链路。当然它也不是没有缺点。纯 Dart 实现意味着高性能场景下会比原生 C 扩展慢一些尤其在做批量签名或大区块头解析的时候这个差距会明显暴露。但我们的场景里更多是低频高价值的交易操作不是高频实时签名服务所以完全够用。真要压榨性能我后面还会讲到配合鸿蒙系统密钥库做双轨签名的方案。经过这轮比较我基本确认了整体技术路线鸿蒙应用包继续用 Flutter 做 UI 和业务编排on_chain 负责链上协议层私钥存储走鸿蒙系统安全能力链上数据缓存走本地数据库。每一层都能独立替换不会因为某个节点供应商挂了就全线崩溃。2. 适配之前先理清三个边界私钥、网络和协议抽象真正动手之前我让团队先把问题拆成三个边界每个边界都要求给出明确答案。这个前置动作很关键否则后面所有代码都会被平台差异牵着走。2.1 私钥到底放在哪一层第一个边界是私钥的存放位置。on_chain 这类纯 Dart 库通常会提供内存中的私钥对象直接参与交易签名。这在普通 App 场景里问题不大但鸿蒙生态对敏感信息的要求普遍更高私钥长期躺在 Dart 堆内存里显然不是最优解。我最后采用的是双轨设计默认情况下私钥可以交给 on_chain 的 Dart 侧对象使用适合临时钱包、测试环境和开发者工具生产环境则通过鸿蒙系统密钥库的能力生成和保存私钥签名时把待签名的哈希交给系统安全组件完成再把签名结果拿回 Dart 层组装交易。这样既保留了 on_chain 原有的 API 便利性又能在重要资产场景里启用更可靠的系统级保护。这两个轨之间并不是完全割裂的。我在 Dart 侧抽象了一个Signer接口on_chain 组装交易前只面向这个接口拿签名结果。底层实现可以是 Dart 内存私钥也可以是通过鸿蒙平台通道调用系统密钥库。上层代码完全无感后续想换更高级的认证方式也不需要改业务逻辑。2.2 网络层是最大变数第二个边界是网络连接。鸿蒙应用默认对网络权限管理很严格在真机上访问公网 RPC 节点前必须在模块配置里显式声明网络权限和明文流量许可否则 HTTP 请求会被静默拦截或直接抛异常。我踩过的一个坑是开发调试时用的某个 RPC 节点是自签名 HTTPS到了鸿蒙真机上证书校验直接失败。排查了半天才发现不是 on_chain 的问题而是系统网络栈默认不信任那个证书链。最后只能让后端同学把证书换成正规 CA 签发的并收紧到只允许连接我们自建的节点网关。所以如果你也要在鸿蒙上接第三方 RPC强烈建议所有请求统一走一个网关服务而不是让客户端直连一堆公网节点。2.3 协议抽象的边界第三个边界是 on_chain 对多链协议的支持方式。我的经验是不要一上来就追求“所有链全部支持”那样库本身的体积和复杂度都会膨胀鸿蒙端编译和热更新都会变慢。先把团队业务真正要覆盖的几条链确定下来然后对照 on_chain 的适配器接口逐一验证地址派生、交易哈希算法、签名恢复等逻辑。如果某个链没有现成适配器也别急着改库核心。更好的做法是在 on_chain 外部扩展一个自己的适配文件按照它的协议约定把地址前缀、交易字段、签名类型映射进去。这样后续 on_chain 升级时可以直接拉新版不需要在自己的分支上维护一堆补丁。移动端的依赖升级本来就该尽量做到能随上上游越少分叉越省心。这三个边界想清楚之后后面的适配工作其实就变成了一件很机械的事情照着接口清单逐项填实现。3. 工程搭建实操让 on_chain 在鸿蒙 Flutter 里跑通第一笔链上查询说干就干。我们从一个最小验证开始目标是让 on_chain 在鸿蒙 Flutter 工程里成功发起一次链上查询比如读一个地址的余额或者拉一个最新区块头。3.1 鸿蒙 Flutter 工程的基础配置与依赖锁定鸿蒙 Flutter 工程的创建流程和常规 Flutter 差不多但要在工程配置里明确指定鸿蒙相关的 SDK 路径。我们团队的做法是在pubspec.yaml里锁定 SDK 约束避免升级依赖时把鸿蒙兼容性冲掉。示例如下environment: sdk: 3.0.0 4.0.0 dependencies: flutter: sdk: flutter on_chain: ^1.0.0 # 其他业务依赖这里最需要注意的是 on_chain 的版本约束不要一上来就写any必须把大版本固定住。因为这个库的接口在跨版本时可能会调整交易对象的结构而鸿蒙侧的可视化调试工具相对没那么完善一旦接口不兼容排查问题的时间成本会很高。添加依赖之后执行flutter pub get如果报错多半是版本冲突。我遇到的情况是 on_chain 传递依赖的某个加密包要求更高的 Dart SDK 版本而鸿蒙 Flutter 工具链自带的 Dart SDK 相对更低。解决方式是升级鸿蒙 Flutter SDK或者在pubspec.yaml中显式锁定那个加密包到兼容版本。升级 SDK 是第一选择因为强制降级加密包可能会引入签名算法的安全问题。3.2 解决 AOT 编译与加密实现冲突鸿蒙 Release 包默认会开启较强的编译优化on_chain 里的一些加密实现如果没有对 AOT 做适配可能在运行时出现奇怪的行为比如整数运算结果不对或者密钥派生失败。排查这一类问题的方法很笨但有效先用 debug 模式跑完全部流程确认数据结果没问题再切到 release 模式逐模块对比。我们在测试中发现了椭圆曲线点运算在 release 包里的结果异常最后定位到是某个数学函数在 AOT 下被过度优化导致的行为差异。解决方式是给那部分计算增加精度约束并在关键运算后做结果自检。final signature customSecureSign(rawHash); if (!verifySignature(signature, rawHash, publicKey)) { throw StateError(Signature verification failed after AOT); }这段代码看起来多余但在鸿蒙 release 包上几乎救了我们一次。AOT 编译下的加密问题不会每次必现而是特定输入才会触发所以增加一层签名自检能早一点把问题暴露出来而不是等到用户资产操作失败后才反馈。3.3 第一个可用验证读取链上余额与区块头跑通依赖和编译之后我习惯先做一个最简验证不碰签名也不碰复杂交易只从链上读数据。这一步能确认网络权限、RPC 地址、序列化解析整条链路是否畅通。final client RpcClient( url: https://your-gateway.example.com/rpc, ); final balance await client.getBalance( address: 0x..., ); final latestBlock await client.getLatestBlockHeader();这个过程中最常见的问题是 node 返回的 JSON 字段与 on_chain 内部期望的数据格式不一致。不要急着改库代码优先看是不是网关做了字段映射。我们自己的网关给部分字段加了额外的包装层级最后在网关侧站点配置里调整对齐之后客户端一行业务代码都不用改。跑通这一步整个鸿蒙化适配的关键路径就证明打通了鸿蒙系统可以运行 on_chain 的 Dart 逻辑网络能够连接到链节点数据可以正确解析。接下来才进入真正敏感的部分——签名与广播。4. 私钥签名链路改造从 Dart 侧到鸿蒙安全能力的双轨协同on_chain 的价值不只是读数据更重要的是构建交易、签名、广播。这个环节是合规和安全要求最高的地方我的建议是永远不要把私钥直接暴露给 UI 层也不要在日志里打印任何签名相关字段。4.1 签名算法族在鸿蒙侧的运行验证我们业务主要用到的是 secp256k1 和 ed25519 两类签名算法。on_chain 内部对这两类算法的实现是纯 Dart 的所以在鸿蒙上可以直接工作。但为了保险起见我让测试同学专门构造了一组已知测试向量把每个算法的签名结果和链上硬编码的期望值做比对。这个测试放在 CI 里每天跑防止某次依赖升级悄悄改了算法行为。测试向量不需要自己发明每个链的官方测试仓库里都有标准样例。你只要把私钥、消息、期望签名值原样喂进去然后断言 on_chain 的签名结果是否一致。这一步能过滤掉绝大部分算法实现层面的兼容问题。4.2 交易构建、离线签名与节点广播的串行链路交易广播的完整链路是从节点拉取最新 nonce 和 gas 参数、构造交易对象、对交易哈希进行签名、把签名结果回填、序列化后广播出去。on_chain 把这几个步骤拆成了清晰的对象模型但鸿蒙侧要注意每个步骤之间的状态不能被并行打乱。比如以太坊系交易里 nonce 必须严格连续。如果你同时发起多笔交易on_chain 的默认逻辑不会帮你维护 pending nonce 队列。我的做法是在业务层加一个非阻塞锁对同一地址的广播操作串行化每笔交易广播后立即更新本地 nonce 缓存。否则一旦出现 nonce 竞争轻则交易被打回重则一笔交易被重复签名导致资产操作混乱。FutureString signAndBroadcastTx({ required PrivateKey privateKey, required UnsignedTx unsignedTx, }) async { final signedTx await walletSigner.signTransaction( privateKey: privateKey, tx: unsignedTx, ); final txHash await rpcClient.broadcastTransaction(signedTx); return txHash; }这里的关键是把walletSigner抽象成可替换对象。在双轨设计下walletSigner内部可能走 on_chain 的 Dart 签名也可能走鸿蒙系统密钥库但外层业务代码永远只看到同一个方法签名。4.3 错误处理与重试机制nonce 竞争、费用估算与链回滚链上交互和数据库事务最大的不同是一旦广播成功你无法回滚只能等待确认或者等它自然失效。因此失败处理策略要保守。我们最终实现的逻辑是这样的广播超时后先查询交易哈希是否已经进入 pending 状态不能简单假定失败。如果确认没有被打包再决定是提高 gas 重发还是重置 nonce 后重新签名。任何一次签名和广播操作都记录操作摘要方便审计。鸿蒙端的内存管理和后台进程回收比其他移动平台更激进签名中间状态要避免长时间驻留在内存里。我的经验是尽量把“签名-广播”收敛成一个短生命周期任务中间不跨页面、不跨异步间隙减少被系统回收的风险。5. 多链资产治理的工程化抽象层、链适配器与数据同步on_chain 解决了“单条链上怎么操作”的问题但资产治理的核心是“多条链的数据怎么统一呈现和调度”。这一层完全在业务代码里鸿蒙化适配的重点是把协议差异隔离在适配器里。5.1 统一资产模型用类型安全压住链差异我先定义一个统一资产模型不直接使用 on_chain 每一条链特有的资产类型。核心字段只有链标识、代币标识、名称、精度、链上余额等。这个模型不关心资产是 ERC-20 还是 SPL只关心它在业务视图里长什么样。这里遇到的最现实问题是精度。有的链用 8 位小数有的链用 18 位直接比较余额前必须统一换算。我的建议是在数据层统一使用高精度大整数存储最小单位只在展示层做小数格式化。这样就不会出现两条链的余额在计算时由于浮点误差而差一分钱的问题。5.2 链适配器注册表与 RPC 端点管理为了让鸿蒙应用能够动态扩展链我维护了一个链适配器注册表。每个适配器负责回答这几个问题地址前缀是什么、交易哈希怎么算、签名算法是什么、RPC 调用接口长什么样、代币转账的数据编码格式是什么。abstract class ChainAdapter { String get chainId; String get coinType; String addressFromPublicKey(Listint publicKey); FutureBalance fetchBalance(String address, Token token); FutureString buildTransferTx({...}); FutureString broadcastRawTx(String rawTx); }这个注册表不需要编译期写死可以从远端配置中心拉取。鸿蒙端每次启动时同步一次适配器清单这样新增链时不用发版只需要在服务端更新注册表。考虑到 on_chain 本身已经内置了很多链的协议逻辑我们的适配器更偏向于“配置映射”而不是重复实现协议细节。5.3 资产变化的持久化与增量同步移动端不可能每次打开页面都去几条链上重新拉全量数据所以我把资产变化做成分层同步策略。第一层是启动时快速同步所有链的轻量余额第二层是用户点击某个资产详情时触发一次深度的交易历史拉取第三层是本地数据库记录每次同步的区块高度作为增量更新的游标。鸿蒙端本地数据库我用的是通用关系型数据库所有资产快照都以链地址加上代币地址作为唯一键。每次同步先查游标再通过 on_chain 调用节点的查询接口拉新增部分最后在事务里批量更新。这样做的好处是离线时也能展示最近一次资产快照不会因为网络抖动导致用户看到一片空白。增量同步还有一个容易被忽略的点链上状态可能在两次同步之间被回滚。尤其是小链上的临时分叉会让上次记录的内部交易失效。我处理的方式是不盲目信任同步游标每次深度同步都会额外拉取一次最新的区块头做对照这个区块头本身就是链上数据可验证性的体现。6. 性能实测与发布检查批量 RPC、权限配置和崩溃兜底开发环境跑通了距离真机发布还差最后一公里。这里说的不是功能而是性能和稳定性。鸿蒙设备的硬件资源差异很大低端机上如果代码写得粗糙链上交互会卡到让人怀疑人生。6.1 批量请求与连接池对吞吐量的提升如果你只是逐个调用 on_chain 的方法发 RPC性能一定不会好看。因为每次请求都有网络往返而区块链节点的 RPC 延迟通常在几百毫秒。我们专门在鸿蒙侧加了一层批量请求代理把同一批地址的余额查询合并成节点的多合约批量调用或者并行发出多个请求然后统一聚合返回。实测下来单地址余额查询从 800ms 左右降到 300ms 左右20 个地址批量聚合查询的总体耗时也从十秒级降到一秒级。但在并行请求时要留意节点的限流策略我们的网关对并发数是有限制的所以连接池里保留了 4 到 8 个长连接超过阈值的请求排队等待。这个数值不是拍脑袋定的是通过压测脚本逐步调整出来的。6.2 隐私权限、应用加固与崩溃现场捕获鸿蒙应用在发布前必须核对权限声明。我们最终只保留了网络访问、本地存储和密钥库相关的最小权限集其他一律不申请。每多一个权限应用审核和用户信任都会多一分风险。签名逻辑的崩溃捕获也比普通功能更需要重视。因为加密相关的异常往往很难从堆栈里直接看出问题我会在签名函数入口处记录流水号在整个签名过程中把关键步骤的输入输出摘要发到远程日志服务。这样一旦出现崩溃可以快速定位是交易构造阶段的问题还是签名字段被篡改的问题。当然日志里绝不能出现私钥内容。所有发送到远程服务的日志都要求先经过脱敏处理地址也要做混淆展示避免任何可还原敏感信息的字段被泄露。6.3 鸿蒙应用上架前需要留意的适配细节上架前的检查清单里除了常规的版本适配和 UI 适配我们还会额外检查后台常驻任务限制。链上交易状态监听如果做成长期后台轮询很容易被系统判定为耗电行为。我的替代方案是只在应用活跃时做 WebSocket 订阅杀进程后依赖服务器推送通知来唤醒用户。另外鸿蒙的多设备协同也给这个项目带来一个好处用户可以在手机上发起签名在平板或电脑上查看详情。on_chain 的交易对象是纯数据结构的可以序列化成 JSON 后在设备间传递签名后的完整交易再广播到链上。这个能力在适配时没有花太多额外功夫完全是纯 Dart 模型带来的红利。7. 踩过几个坑之后我对这套方案的重新认识回到开头说的那个结论。on_chain 在鸿蒙上的适配之所以能走通核心原因是它把链上交互的复杂度压缩在协议层而鸿蒙真正干涉的只是底层运行环境。但这份“顺利”是建立在前期愿意把平台差异显式建模的基础上的。如果一开始就把私钥、网络、协议这三件事揉在一起写后面任何一个环节出问题排查范围都会大到让人崩溃。我最想分享的一点是不要在第一次适配时追求“全部用鸿蒙系统原生能力”。鸿蒙系统密钥库确实安全但它的接口和数据类型与 on_chain 的内存模型差异很大强行做替换会拖慢整个项目的迭代速度。先在 Dart 内存模式里把业务跑通再一层层替换成系统能力这样的增量路径在鸿蒙生态里明显更可控。另外多链资产治理这种功能非常依赖底层库对多链协议的统一抽象。现在回看如果不是选了一个纯 Dart 实现且有适配器结构的库而是选一个靠原生代码堆砌的库这次的鸿蒙化可能根本不会完成。库选对后面所有工作都只是时间和细心问题。这套方案现在已经在我们的内部版本上稳定运行后续还在计划把 on_chain 的链上事件监听能力和鸿蒙的分布式通知能力结合起来做一个跨设备的资产异动提醒。适配过程虽然踩了不少坑但如果下次再让我做一次我一定会更早地把平台通道的边界画清楚而不是等代码写了一半才回头补架构。如果你也在做类似的迁移我的建议很直接把私钥签名、网络通信、多链协议抽象这三条线分开验证任何一条线跑通后再碰下一条千万不要想着一次性在鸿蒙真机上把所有功能调试成功。链上交互这件事错误一旦入了链就没有后悔药所以越是底层的东西越要慢一点稳一点。