最近我接手了一个比较有意思的适配任务:把一个 Flutter 生态里常用的 TON 链上交互库 ton_dart 搬到鸿蒙应用里,还要在实际场景中跑通资产查询、转账还有 TON 治理投票的链路。ton_dart 这个库在 Dart 世界里算是 TON 相关功能最全的三方库之一,钱包、Jetton、NFT、合约交互都有覆盖,可它毕竟是跑在标准 Dart VM 上的,到了鸿蒙原生环境里,网络请求、密钥生成、消息签名这些底子都得重新对接。这篇文章就是把我在模拟项目里真实趟过的鸿蒙化路径、改造成本、关键代码和踩坑点完整写出来,适合要在鸿蒙上做 Web3 钱包、链上治理工具或者资产看板的开发者直接参考。
这套适配的核心思路,不是把 ton_dart 重写一遍,而是做一层“能力替换层”。鸿蒙侧没有 Dart VM 对原生插件的那套自动绑定,但 Flutter 工程可以通过 MethodChannel/Pigeon 和鸿蒙原生代码通信,所以我们可以把 ton_dart 依赖原生能力的部分抽出来,在鸿蒙侧用系统级的 Crypto 和网络能力补上,把纯 Dart 的协议逻辑保留下来。这样既保住了 ton_dart 里成熟的 TON 消息构造、BOC 序列化、地址解析逻辑,又让它在鸿蒙上真正跑得动。下面我从方案选型开始,一步步拆。
1. 项目背景与适配思路拆解
1.1 ton_dart 到底解决了什么问题
如果你对 TON 链上开发有了解,就知道这个生态里最成熟的开发生态其实是 TypeScript,其次有 Python SDK,Dart 这边能用的完整客户端库并不多。ton_dart 是这个生态里少有的全能型选择:它支持从助记词生成、HD 钱包派生,到账户余额查询、Jetton 代币解析、NFT 列表查询,再到构造并发送各种类型的 TON 消息,甚至能对接链上治理相关的提案数据和投票消息。对于 Flutter 团队来说,要在同一套代码里同时覆盖 iOS、Android 和鸿蒙,把业务逻辑写在 Dart 层是最舒服的。ton_dart 的存在意味着合约编解码、地址转换这些纯逻辑可以全部复用,不需要在每个平台上各自写一套 TON 协议的实现。
我在这次适配里最看重的是它的“消息构造”能力。TON 的链上操作本质上是把一笔交易或一条消息编码成二进制格式,然后广播到网络里去。这个过程涉及断言(Cell)、引用(Ref)、BOC 字节流、TL-B 序列化规则,手动实现一遍很容易出错。ton_dart 把这套东西封装成了比较干净的 API,我们只需要传递业务参数,就能得到打包好的消息体。所以适配的第一原则出现了:凡是不依赖平台的纯 Dart 逻辑,原样保留;凡是依赖平台的,坚决替换。
1.2 鸿蒙平台对 Flutter 插件的真实限制
鸿蒙应用目前走的 Flutter 集成路径,和传统 Android/iOS 有一个关键差别:Dart 侧不能直接调用常见的原生插件自动生成的胶水代码,因为鸿蒙的 Flutter 引擎对外提供的通道是拍平过的平台通道。也就是说,你在 Android 上常用的MethodChannel、EventChannel、Pigeon这些机制在鸿蒙上同样存在,只是对端的实现语言变成了 ArkTS,不再是 JVM 或 Objective-C。
ton_dart 原样跑在鸿蒙上的最大阻碍有三个。第一是网络库,ton_dart 默认用的 Darthttp包在鸿蒙上虽然能发请求,但在长连接、证书校验和连接池复用这些环节上,和鸿蒙系统网络栈有兼容差异,遇到复杂链上响应时容易出奇怪现象。第二是安全随机数和密钥管理,助记词、私钥这种东西如果只存在 Dart 侧的内存里,对钱包应用来说是不及格的。第三是签名算法,TON 链上主要用 Ed25519 做消息签名,鸿蒙系统的密钥管理与加解密框架原生支持这个算法,这给了我们用系统能力替代纯 Dart 实现的绝佳条件。
1.3 三方适配方案选型对比
我在正式动手前对比过三条路线。第一是全量重写,在鸿蒙侧用 ArkTS 直接实现 TON 钱包和治理逻辑,好处是没有跨语言的桥接损耗,但坏处是工作量大,而且要重新处理地址、BOC、Jetton 元数据这些繁琐协议,维护成本极高。第二是纯 Flutter 层硬跑,直接把 ton_dart 放进工程,只在鸿蒙侧补网络权限,这条路最快但只适合简单查询,一旦涉及签名和安全的密钥存储,就基本应用不了。第三是桥接式适配,保留 ton_dart 的协议层,把密钥生成、签名、网络请求、安全存储这四类能力桥接到鸿蒙原生实现。
我选了第三种,理由很直接:它能最大程度复用 ton_dart 里成熟的纯 Dart 逻辑,又把最需要平台能力的地方交给了鸿蒙系统框架。桥接的代码量其实不多,核心就是几个方法通道和一套参数约定。相比之下,全量重写的风险高且收益低,而纯 Flutter 硬跑又扛不住真实钱包业务对安全性的要求。适配完成后的整体架构可以简单概括为:Dart 侧跑协议,鸿蒙侧跑系统能力,中间过桥传二进制。
2. 前期准备与工程改造要点
2.1 基线工程与依赖落地
我在模拟项目里用的基线是鸿蒙系统当前主流的 Flutter 混合工程结构,也就是在鸿蒙原生工程里挂一个 Flutter 模块,Flutter 侧保持标准的 pub 依赖管理。ton_dart 目前发布在 pub 仓库上,直接在pubspec.yaml里声明依赖就能拉下来。不过要注意版本收敛问题,ton_dart 对 Dart SDK 版本有要求,如果你项目的 Flutter SDK 太老,要先升级 Flutter 版本再跑依赖解析。
另外要提醒一点,因为 ton_dart 会间接依赖一些常见的 Dart 包,在部分网络环境下依赖拉取会比较慢,建议提前把 pub 源的镜像地址配好,或者把整包离线缓存下来。工程落地时还要在鸿蒙侧的模块描述文件里把网络权限加上,否则后续所有链上请求都会被系统直接拦截,而且这个权限要在真机调试前就确认,不然排查起来很容易绕弯路。
2.2 依赖模块的裁剪与适配工作量对照
拿到 ton_dart 源码后,我没有急着改代码,而是先把它经历的功能模块摸了一遍,用表格把“哪些可以直接用”“哪些需要替换”理清楚。这个动作能帮助团队快速估算工时,也避免在适配过程中釜底抽薪改动核心模型。
| 模块 | ton_dart 原实现 | 鸿蒙侧问题点 | 适配方式 |
|---|---|---|---|
| 地址解析与格式化 | 纯 Dart 逻辑 | 无明显问题 | 原样保留 |
| BOC Cell 序列化/反序列化 | 纯 Dart 实现 | 大数据量下性能一般,但功能正确 | 保留,长消息可在原生侧拼接 |
| BIP39 助记词 | Dart 随机源与词表 | 随机源安全性不够 | 鸿蒙原生生成并返回,Dart 复用词表 |
| 私钥与助记词存储 | 内存对象 | 没有系统级安全存储 | 鸿蒙 KeyStore 能力桥接 |
| Ed25519 签名 | Dart 或依赖包实现 | 需要与鸿蒙密钥库打通 | 鸿蒙 Crypto 框架签名 |
| 网络请求 Toncenter API | Dart http | 连接池、证书链、网络策略差异 | 鸿蒙原生网络栈接管 |
这个表还有一个隐藏作用,就是团队里如果有人想先做一部分,可以照着表格划分任务边界。比如“地址解析”和“BOC 序列化”这两块完全不依赖平台,谁都可以先做;而“签名”和“安全存储”必须和鸿蒙侧一起联调,应该安排到后期集中攻坚。
2.3 权限声明与安全存储设计
在鸿蒙工程里,网络权限需要在前台模块的配置文件中显式声明。如果你在调试时所有链上请求都报系统错误,第一件事就是检查这个权限是否已经添加。除了网络权限,钱包类的应用还在乎一个底线问题:助记词不能被普通内存持有太久。
ton_dart 原生的做法是让助记词和私钥以 Dart 对象形式存在,这在普通 App 里问题不大,但在链上资产管理的场景里风险偏高。我的处理是把助记词的生成和私钥的加载都收口到鸿蒙原生侧,交给系统安全能力托管,Dart 侧只保留一个不落盘的会话句柄。签名时由鸿蒙侧根据句柄找到密钥材料,在系统安全环境内完成签名,Dart 侧只拿到签名结果。这样即使 Dart 层被攻击,私钥材料也不会暴露完整。当然这增加了一层桥接代码,但值得。
3. 核心适配步骤与代码实现
3.1 用 Pigeon 建立稳定的桥接通道
如果方法通道的调用参数只有三五个少得可怜的数量,用 MethodChannel 手写没有问题。但 ton_dart 桥上涉及的参数会包括助记词、签名输入、交易消息、网络返回体,类型复杂且数量多,这时候我建议直接用官方推荐的桥接代码生成工具 Pigeon,把接口定义成类型安全的 Dart 抽象类,再自动生成鸿蒙侧的存根代码。这样无论是 Dart 侧还是 ArkTS 侧都变得可控得多,不用手写一长串字符串类型的 method 名和参数字典。
一个容易被忽略的点是:TON 的业务数据大量是二进制格式,比如打包好的 BOC 字节流,不是普通 UTF-8 字符串。所以桥接协议我建议设计成以Uint8List为主、String为辅,Dart 侧的Uint8List到 ArkTS 侧的Uint8Array有现成的转换规则,但一定要避免中间过程隐式转成字符串又转回来的“假二进制”操作,否则任何一次转码失误都会导致 BOC 数据错位,最终在链上解析成一个完全不同的消息。
3.2 BIP39 助记词与密钥生成的鸿蒙化实现
TON 的钱包助记词遵循 BIP39 标准词汇表,生成流程是先用安全随机源取 128/256 位熵,加校验位后映射成 12/24 个单词。ton_dart 里已经有一整套词表文件和校验算法,这部分纯 Dart 不需要动。真正要换的是“安全随机源”这一环,因为 Dart 侧的随机源强度在不同平台上表现不一致,而生成助记词是资产安全的第一步,必须用系统级安全随机数。
在鸿蒙侧实现时,我用的是一个非常朴素的流程:生成随机熵 -> 传给 Dart 侧的 ton_dart 词表逻辑做校验位补齐和单词映射。Dart 侧拿到的是原始熵字节,也就是 16 个或 32 个随机数,然后调用 ton_dart 里现成的recoverFromMnemonic和generateMnemonic相似的内部逻辑。这里要注意,不要用 Dart 侧自己生成的随机数去生成助记词,否则即使词表逻辑对,随机源强度也不过关。为了保险,我在鸿蒙侧还加了一次最小熵校验,如果系统返回值全零或者固定模式,就直接拒绝继续。
3.3 Ed25519 签名链路的桥接与验签
TON 链上签名用的是 Ed25519 算法,而且签名对象通常不是普通的可读文本,而是“消息 Cell 的哈希”。在 ton_dart 的调用链里,业务方会先构造一个 Cell,取出它的哈希值,再对该哈希做 Ed25519 签名,最后把签名塞回消息的外层结构中。
鸿蒙侧做签名时,我选择把密钥材料托管在系统安全能力里,Dart 侧只传一个“密钥句柄”和“待签名数据哈希”。ArkTS 侧收到请求后调用系统框架的 Ed25519 签名接口,返回 64 字节的签名结果。这里特别容易出错的是字节序和哈希长度,Ed25519 签名输入必须是 32 字节的原文哈希,如果你不小心把哈希转成了字符串再转回二进制,长度和内容都可能变。签名结果也要原样传给 Dart 侧,不能经过 JSON 序列化丢精度。
为了验证桥接后的签名是否可靠,我在本地做了双端验签:Dart 侧用同一个 Elliptic 曲线算法库验一遍签名,鸿蒙侧再验一遍,两边结果必须一致。这个测试看起来简单,却能提前避免很多上链后“签名无效”的悲惨事故。
3.4 网络层接管与消息广播
ton_dart 默认的网络请求能力在普通 Flutter 工程里是够用的,但在鸿蒙化之后,我更建议把网络请求也统一收口到鸿蒙原生侧来做。原因有几点:鸿蒙系统网络栈更擅长处理系统级证书策略;链上接口经常返回较大的 JSON 数据,原生侧解析效率更好;最重要的一点是,TON 网关接口要求 POST 二进制 BOC 数据,内容类型和长度控制得非常严格,原生侧更容易确保请求头的准确组装。
我在鸿蒙侧封装了一个通用的请求方法,专门负责和 TON 生态 HTTP API 网关打交道的 POST/GET 调用。Dart 侧需要发交易时,传入已经在 Dart 层构造好的 BOC 字节流,鸿蒙侧追加 API Key(如果有)和 Content-Type,发起网络请求后把响应字节原样返回 Dart。这样 Dart 侧保持对 ton_dart API 的调用习惯,原生侧掌控连接生命周期和错误重试。实测下来,这种拆分比 Dart 直连稳定得多,尤其在弱网切换场景下表现差距尤为明显。
4. 资产掌控实战:查询、转账与代币识别
4.1 地址解析与余额查询
资产操作的第一步是余额查询。TON 的地址有两种表示:友好地址和非友好地址。日常用户看到的是以EQ或UQ开头的友好地址,但内部编码和合约间通信有时需要非友好形式。ton_dart 自带地址类的解析和转换,这部分完全可以直接保留。
余额查询走的是 TON 生态通用的 HTTP API 网关,请求参数是地址,返回结果里有一个balance字段,单位是 nanoTON,也就是 10 的 9 次方分之一 TON。换算关系并不复杂,但要提醒的是,TON 的精度是 9 位小数,很多新手会把 nanoTON 当成 18 位精度来处理,导致展示金额差出 10 的 9 次方倍,这在资产展示场景里是非常严重的低级错误。我在桥接层里顺手加了一个保留 9 位精度的小数格式化工具,避免上游返回的字符串在 JavaScript 或 ArkTS 的浮点转换里丢精度。
链上余额查询还有个细节:刚部署的钱包合约和已经收过资产的钱包,初始状态不一样。未激活的钱包地址在部分网关接口上会返回空余额或者空交易列表,这种情况会在 UI 上表现为“查不到资产”,其实是合约尚未激活。正确的做法是把它当作余额为 0 处理,并提供“首次转入激活”的提示。
4.2 Jetton 代币与钱包内资产识别
除了原生 TON 币,链上资产里更多的是 Jetton 代币,你可以把它理解为 TON 生态里的可替换代币标准。ton_dart 提供了 Jetton 相关的钱包和元数据解析逻辑,拿到一个 Jetton 钱包地址后,可以向链上合约发起一个只读查询,拿到它的余额和挂在元数据里的代币符号、精度和小数位数。
这里最容易踩坑的是元数据格式。Jetton 的元数据可能是链上全量存储,也可能只是一个指向中心化服务器的链接,即 off-chain metadata。如果元数据是 off-chain 的,那么解析时要额外发一次 HTTP 请求去拉取 JSON,而且这个请求在鸿蒙环境下同样要走网络权限和证书栈。我建议把元数据解析单独封装成一个可以容错的模块:拿到必要字段就展示,拿不到就显示默认占位符,而不是让整个资产列表因某一个代币元数据异常而卡死。
在资产列表中,我还把所有代币的余额统一转换成以 TON 为单位的字符串来展示,内部用整数类型保留精度。浮点型在跨语言桥接里真的不能碰,哪怕一次毫秒级的精度偏差,在资产核对时都会变成可信度灾难。
4.3 转账消息构造与广播
在 ton_dart 里构造一笔 TON 转账,本质上是构造一条内部消息:指定接收地址、转账金额、附加备注,再把它塞进发送方钱包合约的 outgoing message 中,整个消息经过签名和打包,变成一个可以被网关接受的 BOC。这段逻辑属于 ton_dart 的精华,适配时完全保留,我做的只是把签名和网络广播两个环节替换成鸿蒙能力。
一笔转账消息的完整调用链大致是:
// 构造内部消息 final internalMsg = InternalMessage( dest: destAddress, value: TonCoins.fromNano(amountNano), body: TextMessageBody(text: memo), ); // 用钱包合约逻辑添加签名 final wallet = WalletV5(secretKey: secretKey); final externalMessage = await wallet.createTransferMessage( messages: [internalMsg], seqno: seqno, ); // 序列化为 BOC final boc = externalMessage.toBoc();拿到boc字节后,Dart 侧通过桥接层把它交给鸿蒙原生,由原生侧以 POST 方式广播到 TON 网关接口,提交成功后网关会返回交易哈希。注意,TON 的交易广播是“异步确认”的,接口返回哈希只代表网络接受了这个包,不代表交易已经上链,UI 层面必须区分“已广播”和“已上链”两种状态,否则用户会误以为转账一定成功。
5. TON 治理实战:精密提案与链上投票
5.1 从技术视角理解 TON 链上治理
说到 TON 治理,很多人第一反应是“和现实世界里的投票有什么关系”。其实链上治理完全是一个技术流程:网络的重要参数、协议升级、提案的执行都由一组智能合约定义,持币者或验证者通过向这些合约发送特定编码的消息来表达自己的态度。对普通用户来说,参与治理的动作往往就是“对某个提案投一票”,而这一票本质上是一条结构严格的消息。
这套机制天然符合“鸿蒙级链上专家”的主题。一个真正有用的鸿蒙钱包,不应该只做转账,还应该让用户在手机端直接读取提案、构造投票消息、完成签名和链上提交。ton_dart 对这部分的支持主要集中在 Cell 构造和消息签名上,而治理提案的读取则依赖链上数据接口和合约状态解析。
5.2 提案读取与状态解析
读取治理提案时,一般有两个数据来源:一是链上合约存储的提案数据,二是索引服务整理的提案列表。前者更原始,后者更友好。我的做法是在 Dart 侧用 ton_dart 的合约调用封装请求链上提案状态,再把返回的 Cell 数据映射成结构化的提案对象。提案对象至少包含提案编号、提案哈希、经过时间、投票状态、相关合约地址这些字段。
解析过程中最容易出问题的是“提案编号”和“提案哈希”的类型处理。它们可能是大整数或者二进制哈希,跨语言传输时如果走 JSON 字符串接口,一定要明确规定编码格式,我用的是小端十六进制字符串统一表示,两端各自解析,避免出现“0x 到底要不要带”之类的低级分叉。
另外,提案投票窗口是有时间限制的,链上判断是否在窗口内完全依赖时间戳。时区换算这类问题在鸿蒙原生侧有时会受系统时区设置影响,我建议统一使用 UTC 时间戳在 Dart 侧做窗口判断,不要在原生侧依赖本地时区字符串。
5.3 投票消息构造与签名上链
投票消息的构造逻辑可以概括成三步:把投票意向编码进一个 Cell,用签名密钥对该 Cell 的哈希做 Ed25519 签名,再把签名和消息主体一起打包成外发消息。ton_dart 里已经提供了通用的 Cell 构造方法,我们只需要按具体治理提案的规范填字段。
为了节省你们查文档的时间,我整理了一个通用的投票流程伪代码:
// 填入投票目标合约地址 final targetAddress = Address.parse(proposalContractAddress); // 构造投同意票的行为体 final voteCell = beginCell() .storeUint(voteOptionCode, 32) // 投票选项编码 .storeUint(proposalId, 64) // 提案编号 .storeAddress(walletAddress) // 投票人地址 .endCell(); // 用钱包签名并在外层包装 final signedCell = await signCell(voteCell); final externalMessage = await wallet.createTransferMessage( messages: [ InternalMessage( dest: targetAddress, value: TonCoins.fromNano(voteFeeNano), body: signedCell, ), ], seqno: seqno, );这里有个实战心得:提案合约往往对消息体有严格格式要求,任何字段顺序错位都可能导致链上解析失败,而链上错误往往不会给出明确的“字段错位”提示,只会显示一个通用的失败状态。所以务必在公司内部侧提供一个“解码回显”的调试工具,把构造好的消息再从 BOC 反解回字段文本,肉眼确认没有问题再广播。我在模拟项目里就是靠这个工具定位了好几处字段顺序的问题。
投票上链之后,还可以通过提案合约的只读接口查询投票结果权重。这个过程同样可以封装成鸿蒙应用里的一个独立模块,用户投完票立刻能看到当前累计结果,体验会比单纯的广播返回好很多。这种“签名-提交-回读”的闭环,才是把一个普通钱包升级成链上治理工具的关键。
6. 常见问题与排查技巧实录
6.1 高频问题速查表
适配过程中踩过的坑五花八门,我整理了一个高频问题对照表,如果你在实操中也遇到类似现象,可以直接照着查。
| 现象 | 根因 | 解决办法 |
|---|---|---|
| 依赖解析失败 | ton_dart 版本与本地 Dart SDK 不匹配 | 先升级 Flutter/Dart 基线,再重新 pub get |
| 广播 BOC 时报数据无效 | 跨语言传二进制时被转成字符串,丢字节 | 桥接层全程使用字节数组,禁止中间字符串化 |
| 签名结果验签失败 | 签名输入不是 32 字节原始哈希 | 检查取哈希流程,确认结果是 Uint8List(32) |
| 余额显示差 10^9 倍 | 精度单位误当 18 位处理 | 统一按 nanoTON 解析,保留 9 位小数 |
| 助记词生成后无法导入 | 随机熵没有通过安全随机源产生 | 固定使用鸿蒙原生随机数为熵源 |
| 提案投票一直失败 | 消息字段顺序或类型编码不一致 | 构造后先解码回显,再用测试网验证 |
| 网络请求偶发超时 | Dart http 连接池与鸿蒙网络栈兼容问题 | 改由鸿蒙原生网络栈统一发起网络请求 |
这张表在我的适配笔记里作用非常大,相当于一份速查清单,每次联调出现异常时我基本都先用这个清单排除掉最基础的问题,再深入到具体的协议细节里。
6.2 三条值得记住的避坑经验
第一,不要试图在 Flutter 的 isolate 里直接消费 ton_dart 的完整 BOC 解析能力去处理超大的链上消息。Dart isolate 的线程模型和鸿蒙侧的原生线程调度方式存在差异,大对象在 isolate 间复制时会有不可控的延迟和内存峰值。对于消息构造、签名这类高频且对延迟敏感的操作,尽量保持在主 Isolate 或者原生侧完成,不要为了“不卡 UI”而盲目丢给后台 isolate。
第二,字节序问题比想象中更阴险。TON Cell 里很多字段按位存储,跨语言读取时,Bit 的排列顺序和字节序不是同一个概念。我第一次适配时只检查了字节数组的长度,没有检查位级序列化边界,结果在真实提案数据解析时多读了一个 Bit,导致整个后续字段错位。排查了将近一晚上,最后是拿一个已知正确结构的 Cell 做了逐 Bit 对照才找到问题。建议所有字段编解码测试都准备一个“黄金样例”,用已知输入输出对来验证。
第三,测试网络和主网络的数据要严格隔离。鸿蒙应用如果同时支持测试网和主网,最容易出现的问题是签名消息在测试网验证通过,切到主网环境就因为合约版本不一致直接失败。我在配置里把网关地址、合约版本、网络代号全部做成了运行时参数,并且给每一个资产信息和投票入口都打上网络标记,从源头上避免用户误操作。
我个人实际用下来的感受是,鸿蒙化 ton_dart 这件事,最难的地方不在 TON 协议本身,而在于你对两个生态各自边界是否足够清楚。谁负责纯逻辑,谁负责系统能力,边界一旦划清了,工作量大头就只剩桥接层的联调。最后再分享一个小技巧:适配完成后,先做最小闭环验证,从“生成助记词 -> 本地签名 -> 构造消息 -> 广播测试网 -> 回读确认”五步走通,再往钱包页面、治理模块、资产列表这些功能上铺开。比一上来就接一堆特性要稳妥得多,排查问题时也能把变量压缩到可控范围。后续如果你想在这个基础上扩展,可以考虑接入去中心化交易所的聚合跳转、多签钱包,或者更深度的 Jetton 跨合约交互,这些在 ton_dart 的协议层里都已经铺好了地基。