Sway 程序类型(Program Types)完全指南:Contract、Library、Script 与 Predicate 深入解析
【免费下载链接】sway🌴 Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway
Sway 是一种用于构建智能合约的领域特定语言,其程序文件以.sw为扩展名(如main.sw),而文件的第一行必须声明该程序的类型。本篇技术指南以官方参考文档 docs/reference/src/documentation/language/program-types/index.md 为核心骨架,系统讲解 Sway 的四种程序类型(contract、library、script、predicate)、项目类型与入口点规则,并结合仓库内真实示例代码与 forc 源码,帮助你理解每种类型的适用场景、文件结构、代码写法与底层约束。读完本文,你将能够准确判断一个业务需求应选用哪种 Sway 程序类型,并正确组织项目结构与入口函数。
一、Sway 程序的四种类型总览
在 Sway 中,一个程序文件以.sw结尾,且文件第一行必须声明程序类型。总共有四种类型:
| 类型 | 关键字声明 | 典型用途 | 是否可部署上链 |
|---|---|---|---|
| 合约(contract) | contract; | 在固定规则集内运作的协议或系统,如质押合约、去中心化交易所等 | 是(通过交易部署字节码) |
| 库(library) | library; | 封装通用操作的复用代码 | 否(作为其他程序的依赖被引用) |
| 脚本(script) | script; | 复杂、多步骤、且不持久化的链上交互,如通过 DEX 创建杠杆仓位(借款、兑换、再抵押) | 否(仅存在于交易执行期间) |
| 谓词(predicate) | predicate; | 交易构造的前置条件集合,求值结果必须为true交易才有效,如多签谓词 | 否(谓词根(root)在链上作为 UTXO 所有者存在) |
以上四种类型在仓库的 forc 源码中有明确对应。参见 forc/src/utils/program_type.rs 中定义的枚举:
#[derive(Debug)] pub enum ProgramType { Contract, Script, Predicate, Library, }该枚举同时实现了Display,将四种类型映射为字符串"contract"、"script"、"predicate"、"library",这解释了为什么 Sway 源文件第一行要写出contract;、script;这类声明语句——它直接决定了 forc 如何解析、编译与处理该程序。
从编译器的角度理解:Sway 语言本身只提供一套语法,但程序类型决定了生成的字节码形态、可用的指令集(如 predicate 禁止合约指令)、以及对外暴露的入口形态(ABI 还是main())。
二、Sway 项目类型(Project Types)与约束规则
"项目类型"指的是**项目主文件(entry 指定的主文件)**所属的程序类型。因此 Sway 项目同样有四种类型:
- contracts(合约项目)
- libraries(库项目)
- scripts(脚本项目)
- predicates(谓词项目)
关键规则如下:
- 所有四种项目都可以在
src目录下包含多个库文件。也就是说,无论项目主文件是合约、脚本还是谓词,你都可以在src目录中放若干个.sw库文件来组织公共代码。 - 关于合约、脚本与谓词的唯一限制:一个项目最多只能包含合约、脚本、谓词中的任意一种。
- 一个项目不能包含多个合约、多个脚本或多个谓词;
- 也不能在同一项目中混用它们(例如不能同时包含一个合约和一个脚本)。
这条约束决定了项目的边界设计:如果你想同时拥有一个合约和一个脚本,必须拆分为两个独立的 forc 项目,例如仓库中 examples/multi_contract_calls 将callee(被调用合约)与caller(调用方脚本)拆成了两个独立子项目,每个子项目有自己的Forc.toml与src/main.sw。而项目主文件由Forc.toml的[project]段中的entry字段指定,例如:
[project] authors = ["Fuel Labs <contact@fuel.sh>"] entry = "main.sw" license = "Apache-2.0" name = "counter" [dependencies] std = { path = "../../sway-lib-std" }(参见 examples/counter/Forc.toml。)
三、入口点(Entry Points)规则
入口点是程序开始执行的位置,不同类型有完全不同的入口规则:
- 库(library):不可直接部署到区块链,因此没有入口点。库的代码被导出,供其他程序使用。
- 合约(contract):有入口点,对外暴露Application Binary Interface(ABI)——即一组可被外部调用的接口端点。
- 脚本(script):有入口点,暴露一个
main()函数。 - 谓词(predicate):有入口点,暴露一个
main()函数,且该函数必须返回bool。
从代码示例可以直观看到这种差异。脚本与谓词的主文件都以main()为入口(见下文第四、五节),而合约则以 ABI 的实现为入口(见下文第六节)。
四、脚本(Script)详解
4.1 什么是脚本
脚本是一个可执行程序,但不需要部署,因为它只在一次交易执行期间存在。脚本可以复刻合约的功能(例如路由器),却无需承担部署成本,也不会增加链的大小。
脚本的若干属性:
- 不能被合约调用;
- 无状态(stateless),但可以通过合约与链上存储交互(参见 docs/reference/src/documentation/operations/storage/index.md);
- 可以调用多个合约(这也是多合约交互场景如 examples/multi_contract_calls 使用脚本的原因)。
4.2 脚本示例
下面的脚本接收一个参数并返回布尔值true。完整源码位于 docs/reference/src/code/language/program-types/scripts/simple_script/src/main.sw:
script; // All scripts require a main function. The return type is optional. fn main(amount: u64) -> bool { true }注意脚本的要点:
- 第一行声明
script;; - 所有脚本都必须有一个
main函数,其返回类型是可选的(即可以不写返回类型,但通常用于返回交易结果); main函数的参数来自交易的脚本数据区,可以灵活携带调用数据。
4.3 脚本在仓库中的实战佐证
仓库中多个示例都体现了脚本"多步、多合约、无持久化"的典型用法,例如:
- examples/wallet_contract_caller_script/src/main.sw:一个脚本调用钱包合约,演示如何通过脚本对合约发起调用;
- examples/multi_contract_calls/caller/src/main.sw:一个脚本在单次执行中调用多个合约,正是文档所述"can call multiple contracts"的落地案例。
五、谓词(Predicate)详解
5.1 什么是谓词
谓词是一个表示 UTXO 花费条件的可执行程序,例如多签谓词(multisig predicate)。它对可用的 VM 指令有限制。
关键特性:
- 不需要部署到区块链,因为它只在交易期间存在;但谓词根(predicate root)在链上,作为某个或多个 UTXO 的所有者;
- 不能读写任何合约状态;
- 不能使用任何合约指令(contract instructions)。
5.2 向谓词转账
在 Fuel 中,币可以被发送到一个唯一表示某特定谓词字节码的地址——即字节码根(bytecode root)。这意味着"给谓词地址转账"就等于"让该谓词拥有这笔资产"。
5.3 花费谓词资产
币的 UTXO 变得可花费,不是基于提供了有效签名,而是基于以下两点同时成立:
- 提供的谓词其根(root)与 UTXO 的所有者匹配;
- 谓词求值结果为
true。
如果谓词回滚(revert),或尝试访问不纯(impure)的 VM 操作码,则求值结果自动为false。
5.4 花费条件(Spending Conditions)
谓词可以检查花费其资产的那笔交易(inputs、outputs、脚本字节码等),并且可以接收运行时参数(predicateData)。上述任意一项或两者共同影响谓词的求值结果。
5.5 谓词示例
与脚本类似,谓词由一个main()函数构成,可以接收任意数量的参数,但必须返回bool;只有返回true谓词才有效。完整源码位于 docs/reference/src/code/language/program-types/predicates/simple_predicate/src/main.sw:
predicate; // All predicates require a main function which return a Boolean value. fn main(amount: u64) -> bool { true }可以看到谓词与脚本在文件结构上非常接近,核心区别在于:
- 声明关键字不同(
predicate;与script;); - 谓词被用作交易的花费条件,而脚本是主动发起链上操作;
- 谓词受限于不能使用合约指令、不能访问合约状态。
从编译与测试角度看,仓库中 forc-test/test_data/test_predicate/src/main.sw 也提供了可编译、可测试的谓词工程样例,可用于验证谓词构建与求值行为。
六、合约(Contract)详解
6.1 什么是合约
智能合约是一段可以通过交易部署到区块链的字节码。它可以像调用 API 一样被调用,用于执行计算,并像数据库一样存取数据。
一个智能合约由两部分组成:
- Application Binary Interface(ABI):定义合约对外暴露的调用端点;
- ABI 的实现:对接口的具体实现逻辑。
6.2 Application Binary Interface(ABI)
ABI 是一种结构,定义了合约对外暴露的调用端点。也就是说,在 ABI 中定义的函数被视为external(外部函数),合约不能调用自身的这些函数。
下面的例子演示了一个能够接收和发送资金的"钱包"接口。结构以关键字abi开头,后跟合约名,内部是函数签名、存储交互注解(annotations)和文档注释。完整源码位于 docs/reference/src/code/language/program-types/contracts/interface/src/lib.sw:
library; abi Wallet { /// When the BASE_ASSET is sent to this function the internal contract balance is incremented #[storage(read, write)] fn receive_funds(); /// Sends `amount_to_send` of the BASE_ASSET to `recipient` /// /// # Arguments /// /// - `amount_to_send`: amount of BASE_ASSET to send /// - `recipient`: user to send the BASE_ASSET to /// /// # Reverts /// /// * When the caller is not the owner of the wallet /// * When the amount being sent is greater than the amount in the contract #[storage(read, write)] fn send_funds(amount_to_send: u64, recipient: Identity); }可注意到的 ABI 关键要素:
abi关键字 + 名称声明接口;- 函数签名只声明参数与返回类型,不写函数体;
#[storage(read, write)]注解说明该函数与合约存储的交互方式(只读/读写),用于编译器分析与静态检查;- 文档注释描述功能、参数、回滚条件,属于 ABI 元数据的一部分。
6.3 实现 ABI(Implementing the ABI)
实现 ABI 的语法与 Rust 中实现 trait 类似:impl <abi-name> for Contract。
- ABI 中定义的所有函数都必须在实现中声明;
- 由于接口通常定义在合约之外(如上面的
Wallet接口在独立的lib.sw库中),实现前需要先用use语法导入。
完整实现源码位于 docs/reference/src/code/language/program-types/contracts/wallet/src/main.sw:
contract; use interface::Wallet; impl Wallet for Contract { #[storage(read, write)] fn receive_funds() { // function implementation } #[storage(read, write)] fn send_funds(amount_to_send: u64, recipient: Identity) { // function implementation } }6.4 仓库中的完整合约实战样例
仓库提供了大量完整的合约实现,其中最贴近上述文档示例的是 examples/wallet_smart_contract/src/main.sw(完整钱包合约)与 examples/wallet_abi/src/main.sw(钱包 ABI 定义)。前者展示了真实的 ABI 实现细节,包括:
contract; use std::{asset::transfer, call_frames::msg_asset_id, context::msg_amount}; use wallet_abi::Wallet; const OWNER_ADDRESS = Address::from(0x8900c5bec4ca97d4febf9ceb4754a60d782abbf3cd815836c1872116f203f861); storage { balance: u64 = 0, } impl Wallet for Contract { #[storage(read, write), payable] fn receive_funds() { if msg_asset_id() == AssetId::base() { // 收到基础资产时累计余额 storage.balance.write(storage.balance.read() + msg_amount()); } } // ... }这个示例补充说明了文档之外的几个要点:
- 合约可以通过
storage { ... }块声明持久化状态(此处为balance),这正是"合约有状态、脚本与谓词无状态"差异的体现; - ABI 函数可以附加
payable等注解; - 合约实现中可以访问
msg_asset_id()、msg_amount()等上下文函数,而谓词则被禁止使用这类合约相关指令。
此外,examples/counter、examples/storage_map、examples/ownership 等示例覆盖了合约存储、映射与权限控制等常见场景,可作为进一步研读的材料。
七、库(Library)详解
7.1 库的定义
库用于封装执行常见操作的代码,以避免代码重复。库通过文件开头的library;关键字定义:
library;(完整示例见 docs/reference/src/code/language/program-types/libraries/internal/my_lib/src/my_library.sw。)
7.2 可见性与pub关键字
代码可见性规则:库内部——更宽泛地说,Sway 项目内部任何位置——定义的代码默认是private(私有)的,其他文件无法访问,除非被显式暴露。
代码暴露需要两步流程:
- 在代码开头加上
pub关键字; - 在
Forc.toml文件中将目标库声明为依赖,然后通过use导入pub声明。
以下结构可以被标记为pub:
- 全局常量(globally defined constants)
- 结构体(Structs)
- 枚举(Enums)
- 函数(Functions)
- 特征(Traits)
下面是一个完整的库示例,同时展示了pub的用法与不写pub的私有项:
library; // 缺少 pub 关键字,无法被导入 fn foo() {} // 下面这些因为使用了 pub 关键字,都可以被导入 pub const ONE = __to_str_array("1"); pub struct MyStruct {} impl MyStruct { pub fn my_function() {} } pub enum MyEnum { Variant: (), } pub fn bar() {} pub trait MyTrait { fn my_function(); }7.3 库的部署
库不能直接部署到区块链,但可以作为合约的一部分随合约部署——即通过依赖关系被打包进使用它的合约中。
7.4 内部库(Internal Libraries)
如果库与项目的其他程序文件位于同一个src目录下,它就是项目的内部库:
$ tree . ├── Cargo.toml ├── Forc.toml └── src ├── lib.sw └── my_library.sw要在lib.sw中使用内部库my_library.sw,需要两步:
- 使用
mod关键字后跟库名,将库引入作用域; - 使用
use关键字选择性导入库中的各项。
示例见 docs/reference/src/code/language/program-types/libraries/internal/my_lib/src/lib.sw:
library; mod my_library; use my_library::bar; // `bar` from `my_library` is now available throughout the file7.5 外部库(External Libraries)
外部库是位于src目录之外(通常完全是另一个项目)的库:
$ tree . ├── my_library │ ├── Cargo.toml │ ├── Forc.toml │ └── src │ └── lib.sw │ └── my_other_library ├── Cargo.toml ├── Forc.toml └── src └── lib.sw以文档示例为例:
my_other_library:其中定义了使用pub关键字导出的函数quix(),因此可以被my_library导入。源码见 docs/reference/src/code/language/program-types/libraries/external/my_other_library/src/lib.sw:
library; pub fn quix() {}my_library:要在其中使用quix(),同样需要两步。
第一步:添加到依赖(dependencies)。在my_library的Forc.toml文件的[dependencies]段中添加my_other_library。真实配置见 docs/reference/src/code/language/program-types/libraries/external/my_library/Forc.toml:
[project] authors = ["Fuel Labs <contact@fuel.sh>"] entry = "lib.sw" license = "Apache-2.0" name = "my_library" [dependencies] my_other_library = { path = "../my_other_library" } std = { path = "../../../../../../../../../sway-lib-std" }注意此处依赖通过path指定本地相对路径;实际项目中也可以替换为 git 依赖或注册中心(registry)依赖(forc 的依赖解析实现参见 forc-pkg/src/source 目录下的git、path、reg等模块)。
第二步:导入(Import)。使用use关键字选择性导入my_other_library中的代码。源码见 docs/reference/src/code/language/program-types/libraries/external/my_library/src/lib.sw:
library; use my_other_library::quix; // `quix` from `my_other_library` is now available throughout the file7.6 标准库:仓库中库的典型形态
仓库自带的 sway-lib-std 就是"库"这一程序类型的规模化实践:它由 sway-lib-std/src/lib.sw 作为入口,通过模块组织起address.sw、asset.sw、storage.sw、vec.sw、bytes.sw等大量功能模块。各示例工程的Forc.toml都通过std = { path = "../../sway-lib-std" }或std = { git = ... }将其声明为依赖,再以use std::...导入具体功能——这正是"外部库 +pub导出 + 依赖声明"机制的日常体现。
八、如何选择程序类型:决策对照
综合以上内容,在实际开发中可参考以下决策路径:
- 需要持久化状态、被其他合约/脚本/外部调用、承载业务规则→ 选择合约(contract),通过 ABI 暴露接口,注意"合约不能调用自身 ABI 函数"这一约束;
- 需要封装可复用逻辑(常量、结构体、函数、trait)→ 选择库(library),按需声明
pub导出,内部库用mod+use,外部库用依赖 +use; - 需要执行一次性、多步骤、跨多合约的链上操作,且不需要持久化→ 选择脚本(script),编写
main(); - 需要定义 UTXO 的花费条件(如多签、时间锁),通过求值
true/false控制资产花费→ 选择谓词(predicate),编写返回bool的main(),并牢记其不能使用合约指令、不能访问合约状态的限制。
同时要遵守项目级约束:一个项目最多只能包含合约、脚本、谓词中的任意一种,且不能混用;但所有项目都可以自由包含多个库文件。
九、总结
Sway 的四种程序类型(合约、库、脚本、谓词)构成了 Sway 区块链编程的基本骨架:
- 合约是唯一可部署上链、拥有持久状态并以 ABI 为入口的类型;
- 库是纯复用代码载体,通过
pub+ 依赖声明对外暴露能力,内部库用mod引入、外部库用[dependencies]引入; - 脚本提供无需部署的一次性多步链上操作入口(
main()); - 谓词以返回
bool的main()定义 UTXO 花费条件,受限于指令集且不能访问合约状态。
理解这些类型的差异与约束,是正确设计 Sway 工程结构、写出可编译可运行代码的前提。可进一步阅读的仓库资料包括:程序类型分类的源码定义 forc/src/utils/program_type.rs、官方参考文档各子章节(合约、脚本、谓词、库),以及 examples 目录下覆盖各种类型的可运行示例工程。
【免费下载链接】sway🌴 Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考