如何用 AbiFormatter 把 fuels-rs 中的脚本交易解码为可读的合约函数调用
【免费下载链接】fuels-rsFuel Network Rust SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-rs
在 Fuel 链上,合约调用以脚本交易(script transaction)的形式执行。拿到一笔交易的 ID 后,你能看到的只是script字节码和script_data编码数据,无法直接知道它调用了哪个合约方法、传了什么参数。fuels-rs 提供了ScriptType::detect和ABIFormatter两个工具:前者判断一笔脚本交易是合约调用、loader 脚本还是其他脚本,后者在持有合约 ABI 文件时把函数选择器和编码参数还原成可读文本,例如initialize_counter(42)。
本文的主路径是:在 Rust 项目中通过Provider取回一笔脚本交易,判定其类型,再用ABIFormatter解码出函数名和参数。适用环境是已依赖fuels的 Rust 项目,并且你拥有被调用合约的 ABI JSON 文件(由 Sway 构建产物out/release/<contract>-abi.json生成)。
注意命名:文档正文写作
AbiFormatter,但源码中的实际类型名是ABIFormatter,本文代码一律使用ABIFormatter。
准备条件
完成主路径需要三样东西:
- 一笔可查询的脚本交易 ID。最简单的方式是先用 SDK 自己发一笔合约调用,从响应里取
tx_id(示例代码即如此);也可以是对已有链上交易做离线解码。 - 一个连接到节点的
Provider,能够执行get_transaction_by_id。 - 被调用合约的 ABI JSON 文件。解码参数依赖 ABI 中的函数签名与类型定义,没有对应 ABI 就无法还原参数。
相关入口:
- 调试文档描述了这个能力和示例输出;
- 完整可运行示例在 examples/contracts/src/lib.rs 的
decoding_script_transactions测试中; ABIFormatter实现见 packages/fuels-core/src/codec/abi_formatter.rs,ScriptType实现见 packages/fuels-programs/src/debug.rs。
取回交易并判断脚本类型
以下代码来自仓库示例(原样保留setup_program_test!测试脚手架,它负责启动本地节点、生成钱包并部署contract_test合约;project和 ABI 路径指向 fuels-rs 仓库自带的 Sway 测试工程,换成你自己的项目时请替换为你自己的 Sway 合约工程路径):
use fuels::prelude::*; use fuels::programs::debug::ScriptType; setup_program_test!( Abigen(Contract( name = "MyContract", project = "e2e/sway/contracts/contract_test" )), Wallets("wallet"), Deploy( name = "contract_instance", contract = "MyContract", wallet = "wallet" ) ); // 先发起一次合约调用,拿到交易 ID let tx_id = contract_instance .methods() .initialize_counter(42) .call() .await? .tx_id .unwrap(); let provider: &Provider = wallet.provider(); // 第一步:确认取回的是脚本交易 let TransactionType::Script(tx) = provider .get_transaction_by_id(&tx_id) .await? .unwrap() .transaction else { panic!("Transaction is not a script transaction"); }; // 第二步:判断脚本属于哪种类型 let ScriptType::ContractCall(calls) = ScriptType::detect(tx.script(), tx.script_data())? else { panic!("Script is not a contract call"); };ScriptType::detect(tx.script(), tx.script_data())接收交易的script字节码与script_data,返回三种互斥结果:
| 变体 | 含义 | 能拿到什么 |
|---|---|---|
ContractCall(Vec<ContractCallData>) | 合约调用(可能是多个调用) | 每个ContractCallData含contract_id、amount、asset_id、fn_selector_encoded、encoded_args、gas_forwarded |
Loader { script, blob_id } | 通过 blob 加载大合约的 loader 脚本 | blob ID |
Other(ScriptCallData) | 既不是合约调用也不是 loader | 脚本数据data与数据段data_section()(含 configurables) |
文档说明这三种正是 SDK 能区分的脚本交易类别。如果detect的结果不是ContractCall,就无法走函数选择器解码,需要按对应分支处理(见下文)。
用 ABIFormatter 解码函数选择器与参数
确认是合约调用后,每个ContractCallData自带函数选择器(即方法名原文)和编码参数。解码步骤:
let json_abi = std::fs::read_to_string( "../../e2e/sway/contracts/contract_test/out/release/contract_test-abi.json", )?; let abi_formatter = ABIFormatter::from_json_abi(json_abi)?; let call = &calls[0]; let fn_selector = call.decode_fn_selector()?; let decoded_args = abi_formatter.decode_fn_args(&fn_selector, call.encoded_args.as_slice())?; eprintln!( "The script called: {fn_selector}({})", decoded_args.join(", ") );要点:
ABIFormatter::from_json_abi接受 ABI JSON 字符串,内部按函数名建立参数类型表(实现见 abi_formatter.rs 的from_json_abi/from_abi)。decode_fn_selector把fn_selector_encoded字节还原为 UTF-8 函数名字符串。decode_fn_args按 ABI 中的参数类型列表解码encoded_args,返回Vec<String>,每个元素是一个参数的调试格式字符串,可直接拼进日志。- 示例测试对上面的代码打印的输出(文档示例):
The script called: initialize_counter(42)一笔交易可能包含多个合约调用:ContractCall携带的是Vec<ContractCallData>,仓库的 e2e 测试 e2e/tests/debug_utils.rs 展示了多调用场景——call_descriptions.len() == 2时逐个取出选择器与参数解码,两个调用分别得到check_struct_integrity与i_am_called_differently,参数解码结果为类似"AllStruct { some_struct: SomeStruct { field: 2, field_2: true } }"的调试字符串。
其他两种脚本类型的解码方式
主路径之外的两个分支同样基于同一套工具,文档与 e2e 测试给出了对应做法:
loader 脚本:ScriptType::Loader { script, blob_id }分支可以直接读取blob_id([u8; 32]),这是 loader 脚本解码的核心产物。
普通 Sway 脚本(Other):ScriptCallData提供data(脚本参数)和data_section()(configurables 所在的数据段)。e2e 测试 e2e/tests/debug_utils.rs 展示了解码方式:
let ScriptType::Other(desc) = ScriptType::detect(&tb.script, &tb.script_data).unwrap() else { panic!("expected a script"); }; // 解码 main 函数的参数 let args = decoder.decode_fn_args("main", desc.data.as_slice())?; // 解码 configurables(仅当存在数据段时) let configurables = decoder.decode_configurables(desc.data_section().unwrap())?;decode_fn_args对脚本按函数名"main"解码;decode_configurables返回Vec<(名称, 调试字符串)>,例如 e2e 测试断言得到("A_NUMBER", "11")与("MY_STRUCT", "MyStruct { number: 10, boolean: true }")(该测试中的预期值)。脚本没有数据段时data_section()返回None,此时只做参数解码即可。
结果判断与常见报错
解码结果可以直接参与断言,e2e 测试的做法就是比对decode_fn_args返回的字符串向量与预期调试格式(如上文check_struct_integrity的结构体字符串)。
两类报错有明确来源,便于定位问题:
- 函数不在 ABI 中:
decode_fn_args会返回 Codec 错误Function '<fn_name>' not found in the ABI(见 abi_formatter.rs 中的测试)。此时说明选择器解出的函数名与你加载的 ABI 对不上——通常是 ABI 文件与合约二进制版本不一致。 - 选择器不是合法 UTF-8:
decode_fn_selector返回 Codec 错误cannot decode function selector: invalid utf-8 sequence of 1 bytes from index 0(见 debug.rs 中的测试)。这说明交易数据与预期不符,不应按合约调用继续解码。
限制
- 参数解码强依赖 ABI 文件:没有 ABI 时只能拿到函数名(
decode_fn_selector不依赖 ABI),拿不到参数内容。 ScriptType::detect只覆盖文档列出的三类脚本;Other分支下脚本参数必须按函数名(通常为"main")手动指定后才能解码。- 本路径要求交易以
TransactionType::Script形式存在于所连节点;get_transaction_by_id返回None时交易 ID 在该节点上查不到,需换一个能查询到该交易的 Provider。
下一步可以查看 调试文档 与 调试入口页,以及ABIFormatter的 rust 文档(文档提到 configurables 解码的更多细节以 rust docs 为准)。
【免费下载链接】fuels-rsFuel Network Rust SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-rs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考