ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

在 Fuel 合约中使用 TypeScript SDK 解析合约日志(Contract Logs)完整指南

2026/9/10 9:29:43 拓冰建站 浏览量
在 Fuel 合约中使用 TypeScript SDK 解析合约日志(Contract Logs)完整指南 在 Fuel 合约中使用 TypeScript SDK 解析合约日志Contract Logs完整指南【免费下载链接】fuels-tsFuel Network Typescript SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts导读在 Fuel 链上合约方法内部通过 Sway 语言记录log的值会生成一条日志收据log receipt同时该值的类型会被写入合约 ABI。Fuel TypeScript SDKfuels-ts利用这份 ABI 元数据把链上原始的日志收据解码为原生 TypeScript 类型直接呈现在合约调用的返回结果中。本文以 fuels-ts 仓库 apps/docs/src/guide/contracts/logs.md 为主线系统讲解普通logs与按程序合约分组的groupedLogs两种解码结果并覆盖单合约调用、多调用multi-call以及合约间调用inter-contract call三类真实场景读完即可在自己的 DApp 中正确读取合约日志。一、日志在 Fuel 中的产生与落点在 Sway 中合约方法内部调用log()内建函数即可产生日志// apps/docs/sway/log-simple/src/main.sw contract; use log_simple_abi::LogSimple; impl LogSimple for Contract { fn log_simple(val: str[9]) { log(val); } }这段代码来自仓库示例合约 log-simple 合约log_simple接收一个长度固定为 9 的字符串str[9]并立即写入日志。关键点在于链上执行后这笔交易的结果收据receipt中会多出一条Log 类型的收据编译器会把log(val)中被记录值的类型写入合约 ABIfuels-ts 依赖 ABI 中的类型信息完成解码解码逻辑见下文“源码原理”小节日志收据receipt本身不依赖合约返回值一个方法内可以多次调用log()产生多条日志。提示本文展示的 SDK 侧结果均以strings为基础类型但 ABI 解码机制支持 Sway 的多种值类型数值、结构体、枚举、向量等其解码能力由 fuels-ts 的 ABI 编解码模块统一提供。二、Simple Logs用logs读取单合约日志Fuel SDK 将合约调用的响应对象命名为function result。执行一次调用并等待结果后可以从响应中直接读取logs属性其类型为Arrayany。完整代码见仓库片段 logs-simple.ts核心调用链如下import { Provider, Wallet } from fuels; import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from ../../../env; import { LogSimpleFactory } from ../../../typegend; const provider new Provider(LOCAL_NETWORK_URL); const wallet Wallet.fromPrivateKey(WALLET_PVT_KEY, provider); // 部署合约 const deploy await LogSimpleFactory.deploy(wallet); const { contract } await deploy.waitForResult(); // 调用 log_simple 方法 const { waitForResult } await contract.functions .log_simple(ContractA) .call(); // 等待交易结果并解构出 logs const { logs } await waitForResult(); // logs [ // ContractA // ]使用要点logs是所有被调用合约方法产生的日志的扁平数组Arrayany顺序与链上日志收据的顺序一致每个元素的 TypeScript 类型由 ABI 中记录的日志类型决定因此示例中日志ContractA会被直接解码为string该片段由 docs 示例自动生成的合约类型工厂LogSimpleFactorytypegend目录提供类型安全的log_simple方法签名这正是 fuels-ts 类型生成typegen链路的价值连日志方法参数都在编译期被强类型约束。三、Grouped Logs按程序标识符分组日志在真实的 DApp 中一次合约调用可能触发多条日志而多条日志可能来自不同的合约程序。例如Multi-call同一笔交易里对合约多次发起调用Inter-contract call外部合约 A 调用合约 B 的方法二者各自记录日志。为了区分日志的来源fuels-ts 在响应中额外提供groupedLogs属性——它把日志按产生日志的程序标识符Program ID进行分组结果是一个以b256合约地址为 key、以解码后的日志数组为 value 的对象。在 contract-call-script.ts 的调用结果类型中logs与groupedLogs正是作为并列的可选字段返回给上层。3.1 Multi-call 场景对同一个合约做多调用时所有子调用都属于同一个程序 ID因此分组后只有一个 key。见仓库片段 logs-grouped-for-single-contract.tsimport { Provider, Wallet } from fuels; import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from ../../../env; import { LogSimpleFactory } from ../../../typegend; const provider new Provider(LOCAL_NETWORK_URL); const wallet Wallet.fromPrivateKey(WALLET_PVT_KEY, provider); // 部署 LogSimple 合约 const deploy await LogSimpleFactory.deploy(wallet); const { contract: contractA } await deploy.waitForResult(); // 对同一合约做两次调用的 multiCall const { waitForResult } await contractA .multiCall([ contractA.functions.log_simple(Contract1), contractA.functions.log_simple(Contract2), ]) .call(); const { groupedLogs } await waitForResult(); // groupedLogs { // [contractA.id.toB256()]: [Contract1, Contract2] // }观察输出结构key 使用合约地址的 b256 字符串形式contractA.id.toB256()value 为两次调用按顺序累积的日志[Contract1, Contract2]。3.2 Inter-contract合约间调用场景当合约 A 的方法内部调用了合约 B 的方法两边的日志都会被收录此时groupedLogs的价值就完全体现出来了。仓库示例合约 log-inter-calls 合约 演示了这种情况contract; // Interface from the LogSimple contract abi LogSimple { fn log_simple(val: str[9]); } // Interface for the contract abi LogInterCalls { fn log_inter_call(contract_id: b256, simple_log_message: str[9]); } impl LogInterCalls for Contract { fn log_inter_call(contract_id: b256, simple_log_message: str[9]) { log(Starting inter-call); let logger abi(LogSimple, contract_id); logger.log_simple(simple_log_message); log(Inter-call completed); } }执行逻辑为log_inter_call先在自身上下文记录Starting inter-call再通过abi(LogSimple, contract_id)获取外部合约句柄、调用其log_simple最后再记录Inter-call completed。整个过程中 LogInterCalls 合约与 LogSimple 合约各自产生日志。对应 SDK 侧代码见 logs-grouped-for-inter-contract-call.tsimport { Provider, Wallet } from fuels; import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from ../../../env; import { LogInterCallsFactory, LogSimpleFactory } from ../../../typegend; const provider new Provider(LOCAL_NETWORK_URL); const wallet Wallet.fromPrivateKey(WALLET_PVT_KEY, provider); // 1. 先部署一个负责记录简单日志的合约 const deploySimpleContract await LogSimpleFactory.deploy(wallet); const { contract: simpleContract } await deploySimpleContract.waitForResult(); // 2. 再部署发起合约间调用的合约 const deployInterContract await LogInterCallsFactory.deploy(wallet); const { contract: interContract } await deployInterContract.waitForResult(); // 3. 调用 interContract 的 log_inter_call它会回调 simpleContract const { waitForResult } await interContract.functions .log_inter_call(simpleContract.id.toB256(), ContractB) .call(); // 4. 获取按程序分组的日志 const { groupedLogs } await waitForResult(); // groupedLogs { // [simpleContract.id.toB256()]: [ContractB], // [interContract.id.toB256()]: [Starting inter-call, Inter-call completed], // };可见groupedLogs把调用方interContract与被调用方simpleContract的日志分开key 分别是各自的 b256 地址simpleContract只产生了被回调时记录的ContractBinterContract则记录了自己方法体内的两条日志Starting inter-call与Inter-call completed。这正是文档中所强调的“对合约间调用inter-contract或多调用multi-call场景尤其有用”的原因通过程序标识符即可准确归属每一条日志的来源合约。四、源码级原理解析日志如何被解码为 TypeScript 类型logs与groupedLogs不是链上返回的原始字节而是 SDK 依据 ABI 解码后的产物。整个解码依赖两处实现4.1 响应结构中的日志承载在 packages/program/src/response.ts 中函数调用结果FunctionResult的类型定义同时声明了logs?: DecodedLogsunknown[logs]与groupedLogs?: DecodedLogsunknown[groupedLogs]缺省时分别被初始化为空数组[]与空对象{}见 response.ts 第 84-85、91-92 行的处理逻辑。解码发生在合约调用的收据解析阶段SDK 遍历交易收据识别 Log 收据并将其承载的数据交给 ABI 解码器。4.2 ABI 中的 loggedTypes 与 decodeLogSway 编译器会将被 log 类型的描述写入合约 ABI 的loggedTypes字段。在 packages/abi-coder/src/Interface.ts 中decodeLog(data, logId)会先从this.jsonAbiOld.loggedTypes中按logId找到对应的loggedTypedecodeLog(data: BytesLike, logId: string): any { const loggedType this.jsonAbiOld.loggedTypes.find((type) type.logId logId); if (!loggedType) { throw new FuelError( ErrorCode.LOG_TYPE_NOT_FOUND, Log type with logId ${logId} doesnt exist in the ABI. ); } return AbiCoder.decode(this.jsonAbiOld, loggedType.loggedType, arrayify(data), 0, { encoding: this.encoding, }); }每条日志收据中带有一个logId用于关联到 ABI 里声明的日志类型若 ABI 中找不到该logIdSDK 会抛出LOG_TYPE_NOT_FOUND错误这解释了为什么“调用未经过类型生成合约的未声明类型日志”无法解码找到类型后AbiCoder.decode依据 ABI 类型描述把原始字节解码为对等的 TS 值——这就是logs中元素类型由 ABI 决定而非固定string的底层原因。4.3 groupedLogs 的来源归属groupedLogs之所以能按程序分组是因为在解码阶段 SDK 能够同时拿到 Log 收据本身与其所属合约的上下文例如收据中携带的合约 ID / 调用栈信息。在合约间调用与多调用场景下这些收据会被收集汇总后统一解码再以程序标识符为 key 聚合最终产出{ [contractIdB256]: logs[] }结构。docs 示例中的contractA.id.toB256()之所以能作为访问 key正是因为实际分组使用的 key 就是合约 ID 的 b256 字符串。五、注意事项与最佳实践解码依赖 ABI 元数据确保合约调用所使用的接口 / 类型生成产物本文中的LogSimpleFactory、LogInterCallsFactory与链上部署合约的 ABI 一致否则日志类型定位可能失败。logs是扁平收据列表只关心按时间顺序的全部日志时用它需要区分来源合约时务必改用groupedLogs。groupedLogs只覆盖产生了日志的程序没有记录任何日志的合约不会出现在返回对象中例如上面的 multi-call 例子中只有contractA一个 key。字符串等长度类型遵循 Sway 约束示例中的str[9]是固定长度字符串传参超长会在编译期/类型层被约束日志内容也按该类型编码。本地调试环境文档与示例片段使用LOCAL_NETWORK_URL连接本地 Fuel 节点、以WALLET_PVT_KEY创建钱包这两个值定义在 apps/docs/src/env.ts可替换为任意已接入的网络 Provider 与对应的私钥钱包。总结本指南完整覆盖了 fuels-ts 合约日志的两大读取入口面向单合约/扁平顺序日志的logs以及面向多调用、合约间调用、按程序标识符归属日志的groupedLogs。配合仓库中可运行的 Sway 合约log-simple、log-inter-calls与 TS 示例片段logs-simple.ts、logs-grouped-for-single-contract.ts、logs-grouped-for-inter-contract-call.ts开发者可以对照复现上述全部结果并在自己的合约项目如通过fuels typegen生成类型后中按需选用。【免费下载链接】fuels-tsFuel Network Typescript SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考