news 2026/9/9 22:25:28

fuels-ts 脚本日志解析实战:用 `logs` 与 `groupedLogs` 读取 Sway 脚本日志

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
fuels-ts 脚本日志解析实战:用 `logs` 与 `groupedLogs` 读取 Sway 脚本日志

fuels-ts 脚本日志解析实战:用logsgroupedLogs读取 Sway 脚本日志

【免费下载链接】fuels-tsFuel Network Typescript SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts

本文以 Fuel Network TypeScript SDK(fuels-ts)的官方指南 apps/docs/src/guide/scripts/logs.md 为核心,系统讲解如何在 TypeScript 侧解析 Sway 脚本(Script)在执行过程中产生的日志。脚本一旦在 Sway 中调用log(...),就会产生一条写入交易回执(receipt)的日志条目,同时变量类型会记录在脚本的 ABI 中;本指南将演示 SDK 如何借助 ABI 将这些原始数据解析为 TypeScript 类型,并通过脚本调用响应的logsgroupedLogs属性按需读取。读完本文,你将掌握「读取脚本自身日志」与「按合约/脚本来源区分读取分组日志」两种模式,并理解其背后的回执解码链路。

背景:Sway 的log与 SDK 的日志解码机制

在 Fuel 链上,无论是 Sway 合约还是脚本,都可以通过内置的log()函数打印值。从 fuels-ts 的角度看,日志的流转过程是:

  1. 脚本执行到log(value)时,Fuel 虚拟机把日志写入交易结果(TransactionResult)的回执数组(receipts)中;
  2. 日志条目的具体字段类型由程序 ABI 记录;
  3. fuels-ts 拿到回执后,结合本次调用涉及的 ABI(脚本自身 ABI + 被调用合约的 ABI),把原始回执解码成真实的 TypeScript 值。

因此,日志解析的前提条件是代码已通过 typegen 生成类型安全的脚本/合约封装(本文示例中的ScriptLogSimpleScriptLogWithContract即生成产物)。关于脚本实例化与调用的一般流程,可参考 运行脚本指南。

解码结果统一放在脚本调用响应的两个属性中:

  • logs:一维数组,按发生顺序汇总本次调用产生的全部日志;
  • groupedLogs:对象(Record<string, T[]>),按日志来源(合约地址 / 脚本占位地址)分组。

下面分别通过两个官方示例说明。

简单场景:用logs读取脚本自身日志

Sway 侧:一段只打日志的脚本

官方示例脚本位于 apps/docs/sway/script-log-simple/src/main.sw,Sway 代码非常简短:

script; fn main(log_value: str[7]) -> str[7] { log(log_value); log_value }

要点分析:

  • 函数入参log_value: str[7]是一个长度固定的字符串数组(string array)类型str[7]表示恰好容纳 7 个字符;
  • log(log_value)是 Sway 内置日志函数,把该字符串写入回执;
  • 函数随后将入参原样返回,这并不影响日志行为,只是展示「脚本既能返回值、也能附带日志」的常见形态。

TypeScript 侧:读取logs属性

对应 TypeScript 示例见 apps/docs/src/guide/scripts/snippets/script-log-simple.ts,完整代码如下:

import { Provider, Wallet } from 'fuels'; import { WALLET_PVT_KEY, LOCAL_NETWORK_URL } from '../../../env'; import { ScriptLogSimple } from '../../../typegend'; const provider = new Provider(LOCAL_NETWORK_URL); const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider); const script = new ScriptLogSimple(wallet); const { waitForResult } = await script.functions.main('ScriptA').call(); const { logs } = await waitForResult(); // logs: ['ScriptA']

逐步拆解这段代码:

  1. 连接与签名者准备new Provider(LOCAL_NETWORK_URL)建立与本地 Fuel 节点的连接,Wallet.fromPrivateKey(WALLET_PVT_KEY, provider)用测试私钥派生签名者。LOCAL_NETWORK_URLWALLET_PVT_KEY是文档项目测试环境变量(见 apps/docs/src/env.ts 及docs应用的package.json脚本,通常由本地启动的 Fuel 节点提供)。
  2. 实例化脚本封装new ScriptLogSimple(wallet)使用 typegen 生成的类创建脚本调用载体,其方法名与 ABI 中的main一一对应。
  3. 发起调用script.functions.main('ScriptA').call()把字符串参数编码进脚本字节码输入并提交,返回调用句柄。注意'ScriptA'恰好 7 个字符,与 Sway 侧str[7]类型匹配。
  4. 等待出块并取日志await waitForResult()等待交易被确认(也可用 pre-confirmation 流程提前拿到结果),解构出logs属性。

执行后,logs的值就是['ScriptA']—— Sway 里log(log_value)记录的原始值,在 SDK 侧已被还原为同等的 TypeScript 字符串。示例文件末尾还附带了断言逻辑(校验日志长度与内容),用于测试场景:

const expectedLogs = ['ScriptA']; console.log( 'Logs should have the same length', logs.length === expectedLogs.length ); console.log( 'Logs should be in the correct format', expectedLogs.every((log) => logs.includes(log)) );

分组场景:用groupedLogs区分日志来源

当脚本调用合约、而合约内部也打印日志时,仅凭一维的logs很难判断每条日志出自谁。此时应使用响应的groupedLogs属性——它按「日志产生者的地址」把日志归类。

Sway 侧:脚本内跨合约调用并各自打日志

示例脚本位于 apps/docs/sway/script-log-with-contract/src/main.sw:

script; use log_simple_abi::LogSimple; fn main(contract_id: b256) { log("Script started"); let log_contract = abi(LogSimple, contract_id); log_contract.log_simple(__to_str_array("ContractA")); log("Script finished"); }

要点分析:

  • 脚本先log("Script started"),随后通过 ABI 接口LogSimple调用外部合约(入参contract_id: b256是合约地址),让合约打印"ContractA",脚本最后再log("Script finished")
  • 被调合约的 ABI 定义在 apps/docs/sway/log-simple-abi/src/main.sw:abi LogSimple { fn log_simple(val: str[9]); },即接收一个 9 字符的定长字符串并打印;
  • __to_str_array("ContractA")是 Sway 提供的字符串字面量转定长数组辅助函数,配合str[9]使用。

TypeScript 侧:按来源读取groupedLogs

完整示例见 apps/docs/src/guide/scripts/snippets/script-log-with-contract.ts:

import { Provider, Wallet, ZeroBytes32 } from 'fuels'; import { WALLET_PVT_KEY, LOCAL_NETWORK_URL } from '../../../env'; import { LogSimpleFactory, ScriptLogWithContract } from '../../../typegend'; const provider = new Provider(LOCAL_NETWORK_URL); const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider); // Create a contract instance const { waitForResult: waitForDeploy } = await LogSimpleFactory.deploy(wallet); const { contract } = await waitForDeploy(); // Create a script instance const script = new ScriptLogWithContract(wallet); // Call the script const { waitForResult } = await script.functions .main(contract.id.toB256()) .addContracts([contract]) .call(); // Wait for the script to finish and get the logs const { groupedLogs } = await waitForResult(); // groupedLogs = { // [ZeroBytes32]: ['Script started', 'Script finished'], // [contract.id.toB256()]: ['ContractA'], // }

与简单场景相比,这里有三个关键差异:

  1. 先部署被调合约LogSimpleFactory.deploy(wallet)是 typegen 生成的合约工厂,waitForDeploy()返回{ contract },拿到合约实例及其id
  2. 把合约加入调用上下文script.functions.main(contract.id.toB256()).addContracts([contract]).call()中,contract.id.toB256()作为 Sway 侧main的参数(b256地址),.addContracts([contract])则告诉 SDK 本次调用会触及该合约,从而把它的 ABI 纳入日志解码范围。这一步对分组日志的解码至关重要——缺少 ABI,"ContractA"这类值将无法还原类型。
  3. 读取groupedLogs:结果是以 32 字节十六进制地址为键的对象。

脚本没有地址:为何使用ZeroBytes32

需要特别说明的是:脚本(Script)本身没有 ID 或地址,但它仍能产生日志。为了让分组结果统一、可索引,SDK 使用一个全零的十六进制地址(32 字节 b256 常量,即ZeroBytes32,值为0x0000...00)作为「脚本自身」的分组键。因此上面示例的结果结构为:

  • groupedLogs[ZeroBytes32]→ 脚本直接打印的两条:['Script started', 'Script finished']
  • groupedLogs[contract.id.toB256()]→ 被调合约打印的一条:['ContractA']

示例文件尾部同样提供了针对该结构的断言:

console.log( 'Grouped logs should be defined', typeof groupedLogs !== 'undefined' ); console.log( 'Should have zeroed log', typeof groupedLogs[ZeroBytes32] !== 'undefined' ); console.log( 'Should script logs [Script Started]', groupedLogs[ZeroBytes32].includes('Script started') ); console.log( 'Should script logs [Script Finished]', groupedLogs[ZeroBytes32].includes('Script finished') );

在实际应用中,你可以通过判断键是否等于ZeroBytes32来区分「脚本自身日志」与「合约日志」,从而在 UI、事件总线或测试断言中按来源分派处理。

深入底层:logs/groupedLogs是如何解码出来的

了解了用法后,不妨顺着源码确认一下 SDK 内部的处理链路(本仓库内实现路径如下)。

当脚本调用等待结果时,FunctionResult的组装集中在 packages/program/src/response.ts 的buildFunctionResult。其流程是:先transactionResponse.waitForResult()拿到交易结果与回执数组,再通过getAllResultLogs({ receipts, mainCallConfig, functionScopes })同时产出logsgroupedLogs(见该文件第 136–168 行),最终把两者随valuegasUsedtransactionId等一起放入FunctionResult返回。也就是说,logs/groupedLogs并非交易响应的原始字段,而是 SDK 在每次调用后即时解码生成的。

更底层的逐条解码与分组逻辑位于 packages/account/src/providers/transaction-response/getAllDecodedLogs.ts 的getAllDecodedLogs

  • 它遍历每条回执,结合本次调用收集到的全部 ABI(脚本 ABI +.addContracts()传入的合约 ABI),把 Log 数据解码为类型化条目logEntry
  • 解码结果以归约(reduce)方式累积:累积器始终维护{ logs, groupedLogs }logs是平铺的完整列表;groupedLogs[receipt.id]则把每条日志追加到其来源地址(即回执中的id)对应的分组数组中。

从源码结构可以推断,正因为分组键直接取自回执的id字段,而被调合约执行日志时其id即为合约地址,脚本自身的日志则需要一个占位标识——这就是官方文档强调使用零地址ZeroBytes32的原因。

另外,仓库的集成测试也对分组日志有直接覆盖,例如 packages/fuel-gauge/src/advanced-logging.test.ts 等多处用例都解构并断言了groupedLogs,可作为理解实际数据结构与断言的参考。

小结与使用建议

  • 只关心脚本自身打印的值:用脚本调用响应的logs一维数组,按顺序读取即可;前提是传入的参数类型与 Sway 侧的定长类型(如str[7])严格一致。
  • 脚本调用了合约、需要区分日志来源:用groupedLogs,并以ZeroBytes32定位脚本日志、以contract.id.toB256()定位各合约日志;调用时务必通过.addContracts([...])把涉及合约的 ABI 提供给 SDK,否则对应日志无法被正确解码。
  • 注意解码前置条件:日志值能还原为 TypeScript 类型,依赖 typegen 生成的封装类以及本地运行节点(LOCAL_NETWORK_URL)与测试钱包(WALLET_PVT_KEY)就绪;相关运行环境的搭建可参考 部署脚本指南 与 运行脚本指南。

【免费下载链接】fuels-tsFuel Network Typescript SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 22:22:33

Seelen-UI:如何把 Windows 桌面改造成 5 分钟上手的高效率工具

Seelen-UI&#xff1a;如何把 Windows 桌面改造成 5 分钟上手的高效率工具 【免费下载链接】Seelen-UI The Fully Customizable Desktop Environment for Windows 10/11. 项目地址: https://gitcode.com/GitHub_Trending/se/Seelen-UI Seelen-UI 是一套跑在 Windows 10/…

作者头像 李华
网站建设 2026/9/9 22:21:29

DeepSeek接入QQ机器人:多段回复逻辑实现拟人化聊天体验

把 DeepSeek 接进 QQ 群&#xff0c;很多人的第一版做法都差不多&#xff1a;写个脚本&#xff0c;群里发一句话&#xff0c;脚本调一次 API&#xff0c;再把返回结果发回群里。跑通不难&#xff0c;但用两天你就会发现&#xff0c;这个机器人非常“AI”——回复动辄几百字&…

作者头像 李华
网站建设 2026/9/9 22:21:20

栈与队列深度解析:从底层实现到工程实战

1. 先说结论&#xff1a;为什么栈和队列永远值得再聊一遍栈和队列这两个词&#xff0c;刷过题的人闭着眼都能写出几个操作&#xff0c;背八股的人张口就是"后进先出、先进先出"。但真到项目落地的时候&#xff0c;能把它们用得漂亮的人其实没那么多。我看了一圈最近的…

作者头像 李华
网站建设 2026/9/9 22:20:53

SEO检测报告怎么读?墨衍 6 大维度小白解读

标签&#xff1a;SEO检测 SEO检测报告 墨衍 教程 跑完 SEO检测 却看不懂报告&#xff1f;本文按 墨衍 6 大维度&#xff0c;用 小白能懂 的话解读 SEO检测报告 每一栏该干啥。入口&#xff1a;https://mp.csdn.net/seo。 读 SEO检测报告 的顺序 综合建议&#xff08;先抓待办…

作者头像 李华
网站建设 2026/9/9 22:19:38

eBOM到mBOM转化断裂带:从数据到规则的落地路径

设计BOM到制造BOM的“断裂带”&#xff0c;到底断在哪儿&#xff1f;做汽车零部件的老哥们&#xff0c;对下面这个场景应该不陌生&#xff1a;设计部发版了一套新产品BOM&#xff0c;图纸、数模、物料清单全都齐了&#xff0c;看着挺完整&#xff1b;结果BOM流转到工艺部&#…

作者头像 李华