资讯详情

Flutter鸿蒙数字资产钱包适配:JSON-RPC加密管线与签名交易实践

📅 2026/10/18 7:47:54 | 华诺云谱 👁 阅读
Flutter鸿蒙数字资产钱包适配:JSON-RPC加密管线与签名交易实践
1. 项目背景与适配目标为什么 Flutter 里的 ethereum 库到了鸿蒙跑不起来如果你的项目恰好也在做“Flutter 跨端 鸿蒙 HarmonyOS”的数字资产钱包大概率会碰到同一个尴尬官方文档说 Flutter 支持鸿蒙了第三方依赖也能编译过可真要把一笔交易从钱包里签名、上链事情就没那么顺利了。我这次要聊的适配项目正是围绕 Flutter 三方库 ethereum 展开的鸿蒙分布式区块链数字资产上链钱包适配目标是把 JSON-RPC 加密管线完整接通再打通智能合约调用闭环让直播间的 UI 操作真正落到链上。刚开始我拿到这个任务时第一反应是“不就是加一个 MethodChannel 吗”。可真要跑起来才发现问题根本不在“调用原生方法”而在数据流的完整性和密码学操作的边界划分。ethereum 这个库在 Flutter 侧本来能帮你构造交易、做 RLP 编码、推算出地址但它依赖的部分底层能力到了鸿蒙上要么缺失要么行为不一致。尤其是私钥管理、密钥派生、哈希签名这类高敏感操作如果全放在 Dart 内存里无论是性能还是安全隔离都说不过去。这次适配的核心思路是把“Flutter 负责展示和业务流程编排”和“鸿蒙原生负责私钥生命周期 网络传输 链上交互”拆开。UI 层继续用 Dart 写资产余额、转账表单、合约调用入口都可以复用。但一旦牵扯到签名和发送交易就通过平台通道把数据交到 ArkTS 侧处理。这样既保住了 Flutter 的开发效率又让敏感信息只在鸿蒙原生环境里流动整个 JSON-RPC 加密管线的安全性才能立得住。1.1 一个看起来能编译却什么都调不通的现状先说你最容易遇到的现象Flutter 工程能编译ethereum 库也加进了pubspec.yaml但启动后一调用eth_call或者eth_sendTransaction应用就卡住要不就直接崩掉。这种崩溃多半不是因为 Dart 代码写错了而是鸿蒙侧没有对应的网络会话权限、证书校验策略不兼容或者 MethodChannel 传进来的参数类型被原生侧拒绝解析。我试过把交易数据做成 JSON 字符串传过去结果 ArkTS 里解析没问题但 SDK 要求的请求格式里有很多十六进制字符串比如gasPrice和nonce如果 Dart 侧用普通 int 存数值一超过 JavaScript 安全整数范围鸿蒙侧就会拿到一个精度丢失的数字。还有一次是eth_getTransactionCount返回的数据正常但因为我们把chainId写死成了 Decimal 字符串发送交易时节点直接报 -32000。你能想象一个钱包连测试网转账都推不出去是什么感受吗所以这个适配不只是简单的“换个系统”而是在梳理整条资产上链管线的过程中把每一段数据的序列化规则、精度范围、异常语义都重新对齐。Flutter 的生态再好也不能替鸿蒙原生网络栈和系统安全能力做主。真想让 ethereum 库在鸿蒙上干活就得先把这些底层差异摸清楚。1.2 适配的关键把 JSON-RPC 加密管线当成一等公民很多人会把“适配”理解为“让第三方库能编译”但真正的适配目标是“让业务闭环能跑通”。在数字资产钱包这个场景里闭环通常是这样用户输入转账地址和金额应用查询 nonce构造交易本地签名广播到节点轮询回执。整条链路里的每一步都依赖 JSON-RPC 格式的请求和响应。JSON-RPC 本身不复杂无非是jsonrpc: 2.0、method、params、id组成的 JSON 对象。复杂的是它背后承载的密码学操作交易哈希计算、secp256k1 签名、RLP 序列化、合约 ABI 编码。这些操作如果分散在 Flutter 层和鸿蒙层的多个模块里一旦顺序出错链上节点就会拒绝交易。这也是为什么我在设计初期就坚持把“加密管线”当作一个独立模块来对待而不是把签名逻辑散落到 UI 回调里。所谓加密管线我理解是一条从明文业务参数到链上最终确认状态的单向流水线。输入是“转给谁、转多少、附带什么调用数据”输出是“交易哈希 回执状态”。中间每个环节都必须有明确的入参出参格式、错误码和超时策略。只有把这个管线先定义清楚后面写代码才不会越改越乱。这次鸿蒙适配的核心突破就是让这条管线在 Flutter ArkTS 的混合架构里完整跑通。2. 技术选型与分层设计保留 Flutter UI把密码学能力下沉到鸿蒙原生做技术选型之前我先列了一个约束清单第一Dart 侧不允许保存任何私钥第二网络请求必须走鸿蒙原生能力方便后续做网络安全检测第三Flutter 和鸿蒙之间只传结构化数据不互相直接持有可执行对象。基于这三条最终方案确定为“Flutter 业务层 鸿蒙原生加密与网络层”中间用 MethodChannel 通信。为什么这么选首先Flutter 的 UI 跨端优势太明显了钱包里大量表单、列表和动效用 Dart 写比 ArkTS 写省太多时间。其次鸿蒙系统提供了系统级安全存储和统一的网络栈私钥放这里面比放进一个纯 Dart 内存对象里可靠得多。最后ethereum 库虽然也能在纯 Dart 环境里完成签名但它的依赖对鸿蒙兼容不佳强行跑起来容易引入难以排查的崩溃。2.1 分层思路Dart 不该承担私钥生命周期管理我见过一些 Flutter 钱包项目直接在 Dart 里生成私钥用shared_preferences存储或者让用户导出助记词后保存在本地文件。这样做在纯 Android 或 iOS 上可能还能跑但到了鸿蒙上安全审核和系统约束会更严格。私钥如果能在原生层完成生成和存储Dart 层根本不需要看到它整个攻击面就小了很多。所以我在这次适配中把 ethereum 库的使用范围限制在“非敏感”操作上地址格式校验、合约 ABI 编码、交易参数构造、返回数据的解码。真正生成私钥、签名、计算交易哈希的部分全部下沉到 ArkTS 侧实现。Dart 侧通过一个统一的WalletBridge接口与原生通信接口里只有buildTransaction、requestSign、submitRawTx这样的方法完全没有私钥输入参数。这样设计还有一个好处后续如果鸿蒙系统持续升级加密能力比如支持新的安全芯片接口我们只需要改原生侧Flutter 侧的业务代码完全不用动。换句话说Dart 依赖 ethereum 库只负责“生成待签名的原材料”鸿蒙侧负责“把原材料变成链上认可的交易”。2.2 MethodChannel 的数据通道设计MethodChannel 是 Flutter 和原生通信最常规的方式但在鸿蒙上需要特别注意数据类型。标准 MethodChannel 支持 String、num、bool、Map、List 这些基础类型但大整数和字节数组需要额外处理。我在项目里统一规定所有数字类型都以十进制字符串传递所有字节数据以 Base64 或十六进制字符串传递杜绝直接传 int。举个例子Dart 侧构造好一笔交易后传给鸿蒙侧签名的结构是这样final request { action: signAndSend, tx: { nonce: nonce.toString(), gasPrice: gasPrice.toString(), gasLimit: gasLimit.toString(), to: toAddress, value: value.toString(), data: contractData, chainId: chainId.toString(), }, }; final result await _channel.invokeMethodMap(processTransaction, request);鸿蒙侧拿到后先用 BigInt 把字符串转回高精度整数再做后续计算。有人觉得多此一举直接传字符串更麻烦但如果真用双精度浮点几千亿的 Gas 价格就会出现误差。链上交易里一个 wei 的误差都可能让签名结果完全错误。通信层的另一个点是超时和重试。MethodChannel 调用如果长时间不返回Flutter 侧默认会一直等待。我在原生侧设置了 15 秒超时超时后返回一个结构化错误对象。这样 Flutter 侧可以根据错误码提示用户“节点连接超时”还是“交易已被替换”而不是让界面傻等。2.3 管线状态机一次资产上链请求要走完哪些环节把整个加密管线展开后我把它拆成了七个状态参数校验、构造交易、本地签名、交易广播、等待回执、确认上链、失败回滚。每个状态都有明确的进入条件和退出条件。状态输入处理位置输出参数校验地址、金额、合约方法Flutter 侧校验后的业务参数构造交易nonce、gas、dataFlutter 侧 ethereum 库待签名交易结构本地签名待签名交易结构鸿蒙原生侧签名后的 raw transaction交易广播raw transaction 十六进制串鸿蒙原生侧网络请求TX Hash等待回执TX Hash鸿蒙原生侧轮询回执状态确认上链回执中的 blockNumberFlutter 侧成功结果失败回滚错误码和原因双端协作用户可读错误把这个状态机在前端和后端各维护一份两边对账时会非常方便。我在 Flutter 侧用枚举记录当前状态在 ArkTS 侧用回调把最新状态推回 Dart这样用户可以实时看到“签名中”“广播中”“确认中”的不同阶段而不是干等一个加载圈。3. 核心实现从交易构造到智能合约调用的闭环打通理论说完了终于到代码环节。这一部分我拆成四段先用 Flutter 三方库 ethereum 构造交易参数然后交给鸿蒙原生完成签名再讲合约调用 data 的拼装最后说回执轮询。完整跑通后你就能在自己的鸿蒙应用里发起一笔数字资产上链交易并且看到链上的确认结果。3.1 在 Dart 侧用 ethereum 三方库构造交易我在 Flutter 侧引入的是 ethereum 这个三方库它提供了一套相对轻量的交易构建工具。你需要先连接一个 JSON-RPC 节点读取链上的 nonce 和 gas 价格。这里我不建议把节点地址硬编码在 Dart 层而是放到鸿蒙原生的网络配置里方便后续切换测试网和主网。import package:ethereum/ethereum.dart as eth; // 假设已经通过 jsonRpc 拿到了 nonce、gasPrice、chainId final tx eth.Transaction( nonce: nonce, gasPrice: gasPrice, gasLimit: gasLimit, to: toAddress, value: valueInWei, data: contractData, chainId: chainId, ); final rlpEncoded eth.encodeTransaction(tx); final txHash eth.hashTransaction(tx);这套代码本身是平台无关的所以不会直接出错。但你要注意valueInWei和gasPrice必须是 BigInt 类型不能是普通 int。我在项目里写了一个工具函数专门把用户输入的 ETH 金额转成 wei 字符串再交给Transaction构造器。这样后续鸿蒙侧签名才不会因为精度问题产生错误 hash。构造完成后Dart 侧并不直接把整个 RLP 编码后的交易发过去而是把结构体里的字段拆散用字符串形式传给原生。这么做是为了让原生侧能重新检查字段避免恶意调用或参数被意外修改。其实这里也可以直接传输rlpEncoded十六进制串但那样的话原生侧就无法对 nonce、gasLimit 做安全校验了。3.2 鸿蒙原生侧完成签名、RLP 编码和广播鸿蒙侧的签名流程可以分成三步接收交易结构、用本地私钥签名、重新做 RLP 编码。为什么不在 Dart 侧签名一方面私钥不能离开原生存储另一方面鸿蒙提供的加密接口对密钥材料有更好的隔离能力。如果私钥来源是助记词也建议在原生侧完成助记词到私钥的派生。在 ArkTS 里我会先用系统提供的密钥管理能力读取私钥然后对txHash做签名。这里的关键点是签名对象是交易哈希而不是交易全文。这样做既能减少签名数据量又符合以太坊交易签名的标准流程。签名完成后得到 v、r、s 三个值重新拼接出 raw transaction。// 伪代码示意鸿蒙原生侧完成签名和 JSON-RPC 广播 function signAndSend(rawTxFields: Mapstring, string): string { const nonce BigInt(rawTxFields[nonce]); const gasPrice BigInt(rawTxFields[gasPrice]); const value BigInt(rawTxFields[value]); const chainId BigInt(rawTxFields[chainId]); const encoded rlpEncode([nonce, gasPrice, gasLimit, toBytes(value), dataBytes, chainId]); const hash keccak256(encoded); const signature signWithSystemKey(hash); const rawTx rlpEncode([nonce, gasPrice, gasLimit, toBytes(value), dataBytes, chainId, signature.v, signature.r, signature.s]); return jsonRpcCall(eth_sendRawTransaction, [bytesToHex(rawTx)]); }实际开发时RLP 编码和 keccak256 这类算法可以直接找鸿蒙生态里可用的加密库不一定自己从零写。如果暂时没有现成封装可以把以太坊官方实现的 C 库通过 NDK 集成进来。这样虽然会增加包体积但正确性能得到保证。签名返回的原始交易字符串会作为eth_sendRawTransaction的参数发给节点节点只会接受符合规范格式的交易。3.3 智能合约 ABI 编码与 data 字段的拼装智能合约调用比普通转账多的就是data字段。这部分的核心是 ABI 编码把函数名和参数按固定规则打包成十六进制字符串。例如调用一个 ERC-20 的transfer(address,uint256)data 由函数选择器和参数编码拼接而成。函数选择器的计算方式是取函数签名的 Keccak-256 哈希前 4 个字节。transfer(address,uint256)的哈希前 4 字节就是著名的0xa9059cbb。两个参数分别是 32 字节的地址和 32 字节的 uint256地址需要左补零金额也需要转为十六进制后左补零到 32 字节。下面是我在 Dart 侧写的一个拼接逻辑String encodeTransferData(String toAddress, BigInt amount) { final selector a9059cbb; final addressPart toAddress .replaceFirst(0x, ) .padLeft(64, 0); final amountPart amount .toRadixString(16) .padLeft(64, 0); return 0x$selector$addressPart$amountPart; }有一个容易忽略的点地址如果是 EIP-55 校验格式转成纯十六进制时大小写会影响不会因为0x去掉后就是普通十六进制字符。但解析用户输入地址时最好先做一次地址格式校验防止用户粘贴带空格或大小写混乱的内容。构造好data之后把这串十六进制字符串放进Transaction的data字段。如果是为了预估 gas可以使用eth_estimateGas方法传from、to、data等参数拿到的 gas 估算结果再给用户确认。这样能避免因为 gas 设置过高浪费手续费也能防止 gas 设置过低导致交易失败。3.4 回执确认的轮询逻辑与状态返回广播eth_sendRawTransaction成功后节点会返回一个交易哈希。但这并不代表交易已经上链只是表示节点接收了这笔交易。真正确认需要等待矿工把交易打包进区块这笔交易在链上才算真实存在。轮询回执我放在鸿蒙原生层实现因为 Flutter 侧如果频繁发 MethodChannel 调用开销会比较大。原生侧每 3 秒请求一次eth_getTransactionReceipt如果返回的 receipt 里status字段为0x1说明交易成功如果status为0x0表示合约执行回滚如果持续返回 null说明交易还在交易池里等待。轮询不能无限进行我设定最多轮询 20 次超过后返回“交易可能已广播但尚未确认”的状态给用户。有人会觉得“只要拿到 txHash 就算成功”但在数字资产钱包里用户更关心的是“这笔转账是不是真的到账了”。所以回执状态必须完整回传哪怕最后是超时也要如实告诉用户。4. 实测遇到的坑和排查技巧适配过程不可能一次跑通。下面这些坑是真实踩过的我把排查思路也一并写出来。如果你也在做 Flutter 鸿蒙 ethereum 这个组合大概率会遇到其中几个。4.1 MethodChannel 参数类型对 BigInt 不友好第一个坑来自 MethodChannel 的 int 精度限制。Dart 和 ArkTS 都支持 BigInt但 MethodChannel 传输并不支持原生 BigInt 对象。最开始我试着把一个 BigInt 值直接放进 Map结果 Flutter 侧直接抛出异常。后来改成统一用字符串传大数才解决。这个坑在理论上很容易避开但项目里往往会因为大量历史代码而疏漏。排查方法是看崩溃堆栈如果错误出现在invokeMethod调用附近优先怀疑参数类型。建议在工程里做一个统一的BigIntCodec把BigInt序列化成十进制字符串再在原生侧反序列化回BigInt。这比每次手动转换要可靠得多。4.2 RPC 节点返回慢、超时和错误码调用公共 RPC 节点时超时非常常见。我在 ArkTS 侧用了系统网络库发起 POST 请求但默认超时时间偏短节点一忙就中断。后来我把连接超时调到 10 秒读取超时调到 20 秒才算稳定一些。另外不同节点返回的 JSON-RPC 错误码五花八门。有的是-32005表示请求过多有的是-32603表示内部错误还有的直接返回空数组。我建议在原生侧封装一个统一的RpcException把错误码和 message 一起抛给 Flutter 层方便 UI 层做提示。不要让用户看到一串看不懂的原始 JSON。错误排查速查表现象可能原因处理方式交易一直 pendingnonce 太低或 gasPrice 过低重新获取 nonce提升 gasPrice返回 insufficient funds账户余额不足提示用户充值停止后续流程返回 replacement transaction underpriced同 nonce 交易已被接受使用更高 gasPrice 重新签名合约回执 status 0x0合约执行失败检查 data 编码和合约状态MethodChannel 卡死原生侧未回调加超时机制统一错误返回4.3 合约调用返回空数据不一定是失败用eth_call模拟合约调用时如果返回0x不一定代表失败。比如某些只读函数返回空字节数组或者合约本身没有返回值都会得到空结果。一定要结合函数返回类型和 ABI 解码结果判断。我遇到过一种情况调用一个查询函数返回空数据但状态码正常。后来发现是函数签名拼写错误函数选择器不对导致节点根本没有匹配到对应方法。排查这种问题最好的办法是用离线 ABI 工具在本地先验证一遍函数选择器确认编码无误后再上链调用。4.4 签名依赖冲突和崩溃问题Flutter 生态里有不少签名相关的库很多底层都依赖原生加密实现。在鸿蒙上有些库并没有适配强行使用会出现UnsatisfiedLinkError或者直接闪退。我一开始尝试在 Dart 侧用某个开源库做 secp256k1 签名结果在鸿蒙模拟器上完全跑不通。后面我把签名逻辑全部移到了鸿蒙原生侧用系统加密接口处理问题就消失了。这也验证了最初的分层思路不要把平台相关能力硬塞进一个跨端库。遇到类似崩溃先看崩溃发生在哪个底层 so 或 dylib 调用再确认该类库是否有鸿蒙版本。实在没有就改写原生侧实现。5. 这次适配沉淀下来的经验与后续扩展最后说点个人体会。这个项目最让我意外的不是技术难度而是“边界划分”带来的收益。当我决定让 Flutter 只做 UI 和流程编排、鸿蒙原生负责敏感操作后很多曾经很棘手的问题比如私钥泄露、签名库崩溃、数据精度丢失都自然消失了。不是它们不存在了而是被限制在一个更可控的范围内。如果你也想做类似的 Flutter 鸿蒙数字资产钱包适配我建议先从一条最简单的“查询余额 转账”链路开始跑通后再加入智能合约交互。不要一上来就急着做完整的 DApp 浏览器或复杂的多签合约逻辑。因为数据管线的复杂度是逐步累积的一次接入太多功能排查问题时会非常痛苦。5.1 一些关于安全边界的个人体会数字资产上链操作不可逆所以安全边界怎么强调都不过分。我的原则是私钥不经过 Dart 层、助记词不落本地明文、签名结果不写日志、交易内容需要在 UI 上二次确认。这些规则写进规范里容易执行起来难因为每个改动都可能影响开发效率。但再麻烦也值得。鸿蒙系统的安全能力本来就是一大强项如果因为开发便利把私钥放在 Flutter 侧反而浪费了平台优势。真正合理的状态是Flutter 侧永远拿不到私钥用户看到的只是“签名中”“已广播”这样的状态而无法从应用数据目录里翻出任何密钥文件。另外在使用测试网做验证时尽量选择官方推荐的测试网络并且不要在里面放有真实价值的数字资产。智能合约交互尤其要小心测出问题可以不断重来但如果误操作把资产发送到错误地址结果是不可逆的。5.2 后续可以怎么扩展这次打通的是一个最基本的 JSON-RPC 加密管线基于同样的架构后面还能扩展不少能力。比如多链支持可以把“节点配置”抽象成ChainConfig在不同链之间切换比如 DApp 内置浏览器可以把合约调用从“预先拼装 data”升级为“动态解析用户输入的 ABI”比如更细粒度的权限控制把某些高风险交易二次校验流程做得更严格。还有一个方向是把管线做成插件化把“构造交易”和“签名”两个阶段拆成独立服务方便以后接入硬件钱包或者系统级安全芯片。这个项目的核心价值不在于某一串代码而在于把一个原本松散的第三方库调用梳理成一条路径清晰、安全性可控的资产上链闭环。至少对我个人来说下次再换一个平台做适配我都会先画出一条管线再开始写代码而不是拿到一个第三方库就直接往工程里堆。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑