☰
鸿蒙适配Flutter链上交互库on_chain:私钥签名与多链资产治理实践
2026/10/22 14:30:46 网站建设 项目流程

把 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 竞争,轻则交易被打回,重则一笔交易被重复签名导致资产操作混乱。

Future<String> 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(List<int> publicKey); Future<Balance> fetchBalance(String address, Token token); Future<String> buildTransferTx({...}); Future<String> 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 的链上事件监听能力和鸿蒙的分布式通知能力结合起来,做一个跨设备的资产异动提醒。适配过程虽然踩了不少坑,但如果下次再让我做一次,我一定会更早地把平台通道的边界画清楚,而不是等代码写了一半才回头补架构。

如果你也在做类似的迁移,我的建议很直接:把私钥签名、网络通信、多链协议抽象这三条线分开验证,任何一条线跑通后再碰下一条,千万不要想着一次性在鸿蒙真机上把所有功能调试成功。链上交互这件事,错误一旦入了链就没有后悔药,所以越是底层的东西,越要慢一点,稳一点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询