Sway 程序类型(Program Types)完全指南:Contract、Library、Script 与 Predicate 深入解析
2026/9/12 17:38:56 网站建设 项目流程

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(谓词项目)

关键规则如下:

  1. 所有四种项目都可以在src目录下包含多个库文件。也就是说,无论项目主文件是合约、脚本还是谓词,你都可以在src目录中放若干个.sw库文件来组织公共代码。
  2. 关于合约、脚本与谓词的唯一限制:一个项目最多只能包含合约、脚本、谓词中的任意一种
    • 一个项目不能包含多个合约、多个脚本或多个谓词;
    • 也不能在同一项目中混用它们(例如不能同时包含一个合约和一个脚本)。

这条约束决定了项目的边界设计:如果你想同时拥有一个合约和一个脚本,必须拆分为两个独立的 forc 项目,例如仓库中 examples/multi_contract_calls 将callee(被调用合约)与caller(调用方脚本)拆成了两个独立子项目,每个子项目有自己的Forc.tomlsrc/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 变得可花费,不是基于提供了有效签名,而是基于以下两点同时成立:

  1. 提供的谓词其根(root)与 UTXO 的所有者匹配;
  2. 谓词求值结果为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(私有)的,其他文件无法访问,除非被显式暴露。

代码暴露需要两步流程

  1. 在代码开头加上pub关键字;
  2. 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,需要两步:

  1. 使用mod关键字后跟库名,将库引入作用域;
  2. 使用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 file

7.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_libraryForc.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 目录下的gitpathreg等模块)。

第二步:导入(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 file

7.6 标准库:仓库中库的典型形态

仓库自带的 sway-lib-std 就是"库"这一程序类型的规模化实践:它由 sway-lib-std/src/lib.sw 作为入口,通过模块组织起address.swasset.swstorage.swvec.swbytes.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),编写返回boolmain(),并牢记其不能使用合约指令、不能访问合约状态的限制。

同时要遵守项目级约束:一个项目最多只能包含合约、脚本、谓词中的任意一种,且不能混用;但所有项目都可以自由包含多个库文件。

九、总结

Sway 的四种程序类型(合约、库、脚本、谓词)构成了 Sway 区块链编程的基本骨架:

  • 合约是唯一可部署上链、拥有持久状态并以 ABI 为入口的类型;
  • 是纯复用代码载体,通过pub+ 依赖声明对外暴露能力,内部库用mod引入、外部库用[dependencies]引入;
  • 脚本提供无需部署的一次性多步链上操作入口(main());
  • 谓词以返回boolmain()定义 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询