用 Sway 实现智能合约版 FizzBuzz:从 ABI 设计到链上调用
【免费下载链接】sway🌴 Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway
Sway 语言官方书籍的 FizzBuzz 示例 展示了一种与经典编程题目不同的玩法:它不是跑在本地终端上的控制台程序,而是一个部署在 Fuel 区块链上的智能合约——脚本通过 ABI 调用合约的fizzbuzz方法,传入u64数值,合约返回一个表示结果的枚举(enum)。读完本文,你将掌握如何用 Sway 的contract程序类型、abi声明与impl实现来组织一个最小可用的链上逻辑,理解枚举与结构体如何自动进入 ABI JSON 供链下代码解码,并能够结合forc构建、部署与调用这一示例。
FizzBuzz 题目的智能合约化改造
经典 FizzBuzz 的规则众所周知:从 1 数到 N,遇到 3 的倍数输出Fizz,遇到 5 的倍数输出Buzz,同时是 3 和 5 的倍数输出FizzBuzz,其余输出数字本身。原文档明确指出,本例不是传统意义上的 FizzBuzz,而是它的智能合约版本:
This example is not the traditional FizzBuzz; instead it is the smart contract version! A script can call the
fizzbuzzABI method of this contract with someu64value and receive back the result as anenum.
二者的核心差异在于交互模型:传统程序把结果打印到标准输出,而这里的合约把结果编码为返回值返回给调用方。这意味着“打印”这个动作被替换成了“返回一个可被链下代码解码的类型化值”,而“循环计数”则被压缩为对单次输入的判定。整个示例浓缩了 Sway 智能合约开发的完整骨架:
- 用
contract;声明程序类型; - 用
enum定义返回的数据结构; - 用
abi声明对外接口; - 用
impl <ABI> for Contract实现接口逻辑。
完整源码位于 examples/fizzbuzz/src/main.sw,项目清单文件位于 examples/fizzbuzz/Forc.toml。
完整示例源码与逐行拆解
程序类型声明
contract;Sway 中的每个程序文件都必须以程序类型声明开头。Sway 程序共分为四类:contract(合约)、script(脚本)、predicate(谓词)和library(库),前三种可部署到链上,而库仅用于代码复用、不会被直接部署(参见 程序类型概览)。本例声明为contract,意味着这是一个可被调用、可持有状态的链上程序。
用枚举建模结果
enum FizzBuzzResult { Fizz: (), Buzz: (), FizzBuzz: (), Other: u64, }FizzBuzzResult是一个包含四种变体(variant)的枚举,即求和类型(sum type)。前三个变体Fizz、Buzz、FizzBuzz的类型是单元类型(),表示“只有该变体本身、不附带额外数据”;Other: u64则携带原始输入数值,用于“既不是 3 的倍数也不是 5 的倍数”的情况。
从内存布局看,枚举需要一定的额外开销:为了区分当前保存的是哪个变体,Sway 会为枚举存储一个 8 字节(一个 word)的 tag,其后预留的空间等于最大变体的大小。在本例中,最大变体是Other: u64(8 字节),因此每个FizzBuzzResult值在内存中占用约 16 字节(8 字节 tag + 8 字节数据)。更多细节参见书籍的 结构体、元组与枚举 章节。
ABI 声明:定义对外接口
abi FizzBuzz { fn fizzbuzz(input: u64) -> FizzBuzzResult; }abi声明定义了一个名为FizzBuzz的 Application Binary Interface(应用二进制接口),其中只有一个方法fizzbuzz:接收一个u64参数input,返回FizzBuzzResult。注意 ABI 声明里只有函数签名、没有函数体,这与 trait 声明 类似。对于使用 ABI 的合约,它必须定义或导入 ABI 声明并实现它(参见 什么是智能合约)。
实现 ABI:落实判定逻辑
impl FizzBuzz for Contract { fn fizzbuzz(input: u64) -> FizzBuzzResult { if input % 15 == 0 { FizzBuzzResult::FizzBuzz } else if input % 3 == 0 { FizzBuzzResult::Fizz } else if input % 5 == 0 { FizzBuzzResult::Buzz } else { FizzBuzzResult::Other(input) } } }impl FizzBuzz for Contract把 ABI 实现到当前合约上。for Contract语法专用于为合约实现 ABI;如果是为结构体实现方法则应使用impl Foo语法。判定逻辑本身就是一个多分支if表达式:
input % 15 == 0:同时被 3 和 5 整除,返回FizzBuzz(先判断这一项,避免被更具体的分支提前拦截);- 否则
input % 3 == 0,返回Fizz; - 否则
input % 5 == 0,返回Buzz; - 否则把原始输入包进
Other变体返回:FizzBuzzResult::Other(input)。
这段逻辑依赖 Sway 的if表达式分支能力。Sway 中if是表达式而非语句,可以用在let绑定的右侧;同时所有分支必须返回同一类型,本例各分支返回的正是同一个FizzBuzzResult类型(参见 控制流)。最终每个分支都返回一个值,这也与“枚举携带数据”的特性自然契合——空变体直接构造,带数据变体用枚举名::变体名(值)语法构造。
ABI JSON:链下代码如何理解链上返回值
原文档特别强调了一个关键事实:
The format for custom structs and enums such as
FizzBuzzResultwill be automatically included in the ABI JSON so that off-chain code can handle the encoded form of the returned data.
也就是说,FizzBuzzResult这样的自定义枚举(以及结构体)的类型格式会自动被写入 ABI JSON。forc build生成 ABI JSON 时,会自动序列化合约接口以及接口中出现的全部自定义类型定义。链下代码(如 Rust SDK、TypeScript SDK 或任何能解析 JSON 的客户端)读取该 JSON 后,即可知道返回值FizzBuzzResult有哪几个变体、每个变体的负载类型是什么(()或u64),从而正确解码链上返回的编码字节,还原出是Fizz、Buzz、FizzBuzz还是Other(u64)。
这正是 Sway 合约“接口即类型系统”的体现:合约方法与链下客户端通过 ABI JSON 达成类型层面的契约,枚举/结构体格式被自动包含,调用方无需手工维护解码表。ABI 的声明、实现与调用方式可进一步参考 什么是智能合约 章节中关于 ABI 声明与impl Self的说明。
脚本调用合约:把结果拿回链下
合约部署后,需要一个调用方。原文档指明“脚本(script)可以调用该合约的fizzbuzzABI 方法”。在 Sway 中,脚本是只执行一次的可运行字节码,不持有资源、也不能被合约调用,但可以调用合约并根据返回值继续执行(参见 脚本)。
脚本通过ABI 转换(abi cast)来调用合约,核心写法如下(示例见 examples/wallet_contract_caller_script/src/main.sw):
script; use wallet_abi::Wallet; fn main() { let contract_address = 0x9299da6c73e6dc03eeabcce242bb347de3f5f56cd1c70926d76526d7ed199b8b; let caller = abi(Wallet, contract_address); // ... caller.send_funds { gas: 10000, coins: 0, asset_id: b256::zero() }(amount_to_send, recipient_address); }abi(AbiName, contract_address)返回一个ContractCaller,ABI 的方法成为其可用方法。调用时可以指定三个可选的特殊参数:gas(转发给合约的 gas,默认取上下文 gas 即$cgas寄存器)、coins(随调用转发的币数,默认 0)、asset_id(转发资产的 ID,默认b256::zero())。对 FizzBuzz 合约来说,脚本只需abi(FizzBuzz, address)构造调用者,然后调用caller.fizzbuzz { gas: ..., coins: 0, asset_id: b256::zero() }(15)之类的表达式,即可拿到FizzBuzzResult枚举值,再按其变体分支处理。forc会把脚本与合约一起编译进交易,脚本在交易执行时完成这次链上调用。
工程配置:Forc.toml中的关键信息
示例的项目清单 examples/fizzbuzz/Forc.toml 内容如下:
[project] authors = ["Fuel Labs <contact@fuel.sh>"] entry = "main.sw" license = "Apache-2.0" name = "fizzbuzz" [dependencies] std = { path = "../../sway-lib-std" }entry = "main.sw":指明入口文件为src/main.sw(默认入口为main.sw,此处显式声明);name = "fizzbuzz":包名,合约构建产物(字节码与 ABI JSON)都会以此命名;std依赖通过相对路径指向仓库根目录的 sway-lib-std 标准库,这是因为本示例位于 Sway 主仓库内部、直接复用同一源码树的 std;普通独立项目则通常写成std = { git = "..." }或std = { version = "..." }形式。
examples目录下的全部示例由顶层的 examples/Forc.toml 以 workspace 方式统一组织管理,其中fizzbuzz是默认启用的成员之一。构建时在examples/fizzbuzz目录下执行forc build即可生成合约字节码与 ABI JSON;也可以从仓库根目录对整个 workspace 执行forc build(构建方式参考 forc 构建命令)。
从源码看该示例在仓库中的双重存在
在 Sway 仓库中,FizzBuzz 逻辑实际上出现了两次,用途不同:
1. 书籍示例(contract 版本):即本主题文档讲解的对象,位于 examples/fizzbuzz/src/main.sw,被 docs/book/src/examples/fizzbuzz.md 通过{{#include}}指令直接嵌入书籍页面,强调“枚举类型自动进入 ABI JSON”这一链上契约特性。
2. 语言参考手册示例(library 版本):位于 docs/reference/src/code/examples/fizzbuzz/src/lib.sw,是一个library;类型的程序,把判定逻辑封装成普通函数fn fizzbuzz(input: u64) -> State,枚举命名为State(变体Fizz/Buzz/FizzBuzz/Other(u64),与合约版一致),对应文档 docs/reference/src/documentation/examples/fizzbuzz.md。两处共享完全相同的判定规则:
- 能被 3 整除返回
Fizz- 能被 5 整除返回
Buzz- 同时能被 3 和 5 整除返回
FizzBuzz- 其余情况原样返回输入
将同一算法分别以“库函数”和“合约 ABI 方法”两种形态呈现,恰好对比出 Sway 程序类型的使用差异:库用于链下/链上通用的代码复用,合约则把逻辑暴露为可被外部调用的链上接口。
小结与延伸
本文围绕 Sway 官方书籍的 FizzBuzz 示例,完整拆解了智能合约版 FizzBuzz 的四个组成部分——contract;程序类型声明、带数据的枚举返回值、abi接口声明与impl for Contract实现,并说明了自定义枚举如何自动进入 ABI JSON 供链下客户端解码,以及脚本如何通过abi转换调用合约。若想继续深入,可以:
- 阅读 Counter 示例 了解带持久化存储的合约;
- 阅读 Wallet 智能合约示例 了解多方法 ABI 与资金转移场景;
- 结合 控制流 与 结构体、元组与枚举 加深对
if表达式与求和类型的理解。
【免费下载链接】sway🌴 Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考