news 2026/9/12 4:47:37

Sway 钱包智能合约实战:ABI 声明与合约实现的双项目架构

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Sway 钱包智能合约实战:ABI 声明与合约实现的双项目架构

Sway 钱包智能合约实战:ABI 声明与合约实现的双项目架构

【免费下载链接】sway🌴 Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway

本文基于 Sway 官方文档 Wallet Smart Contract 示例,讲解如何构建一个典型的 Sway 钱包合约:将 ABI 接口声明抽离为独立库项目,由合约项目以路径依赖方式引用并实现。读完本文,你可以掌握abi声明、#[payable]/#[storage]属性、std库消息上下文函数与transfer转账机制的配合方式,以及 Checks-Effects-Interactions 防重入模式的落地细节。

一、总体架构:ABI 声明与实现分离

Sway 中 ABI 接口声明与 ABI 实现分属两个独立的 Forc 项目。wallet_abi项目是一个library,只声明接口契约;wallet_smart_contract项目是contract,负责具体实现。官方文档给出的目录结构如下(对应仓库中的 examples/wallet_abi 与 examples/wallet_smart_contract):

. ├── wallet_abi │ ├── Forc.toml │ └── src │ └── main.sw └── wallet_smart_contract ├── Forc.toml └── src └── main.sw

其中wallet_abi被当作外部库使用。使用外部库时,必须在项目自身的Forc.toml中声明依赖来源。wallet_smart_contract项目中的声明为:

[dependencies] wallet_abi = { path = "../wallet_abi/" }

仓库中实际的 Forc.toml 完整内容如下,可以看到除了wallet_abi路径依赖外,还显式声明了std标准库依赖(指向本仓库的 sway-lib-std 源码目录):

[project] authors = ["Fuel Labs <contact@fuel.sh>"] entry = "main.sw" license = "Apache-2.0" name = "wallet_smart_contract" [dependencies] std = { path = "../../sway-lib-std" } wallet_abi = { path = "../wallet_abi" }

而 ABI 库项目自身的 Forc.toml 仅声明std依赖,因为纯接口声明本身不依赖合约逻辑。

二、ABI 声明:定义合约对外契约

ABI 声明位于 examples/wallet_abi/src/main.sw,完整代码如下:

library; abi Wallet { #[storage(read, write), payable] fn receive_funds(); #[storage(read, write)] fn send_funds(amount_to_send: u64, recipient_address: Address); }

关键要素说明:

  • library;:声明该项目为库而非合约/脚本,其产物只被其他项目以依赖方式引用;
  • abi Wallet { ... }:定义名为Wallet的接口契约,其中的函数只有签名,没有函数体。任何合约只要impl Wallet for Contract就必须按签名提供实现;
  • #[storage(read, write), payable]receive_funds允许读写 storage(入账需要累计余额),且标记为payable,即调用方可以随调用发送资产;
  • #[storage(read, write)]send_funds同样需要读写 storage(扣减余额),但不是payable——转账本身不依赖随调用发送的资产。

三、ABI 实现:合约完整源码

合约实现位于 examples/wallet_smart_contract/src/main.sw,完整代码如下:

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() { // If we received the base asset then keep track of the balance. // Otherwise, we're receiving other native assets and don't care // about our balance of coins. storage.balance.write(storage.balance.read() + msg_amount()); } } #[storage(read, write)] fn send_funds(amount_to_send: u64, recipient_address: Address) { let sender = msg_sender().unwrap(); match sender { Identity::Address(addr) => assert(addr == OWNER_ADDRESS), _ => revert(0), }; let current_balance = storage.balance.read(); assert(current_balance >= amount_to_send); storage.balance.write(current_balance - amount_to_send); // Note: `transfer()` is not a call and thus not an // interaction. Regardless, this code conforms to // checks-effects-interactions to avoid re-entrancy. transfer( Identity::Address(recipient_address), AssetId::base(), amount_to_send, ); } }

3.1 存储与常量

  • storage { balance: u64 = 0 }:定义一个持久化存储字段balance,初始值为 0。在合约中通过storage.balance.read()/storage.balance.write(...)访问,这正是 ABI 声明中#[storage(read, write)]属性的实现依据——编译器会校验实现侧对 storage 的访问不超出声明范围;
  • OWNER_ADDRESS:以 32 字节十六进制字面量构造Address,作为唯一有权限提取资金的地址。

3.2 receive_funds:入账记账

receive_funds逻辑:

  1. msg_asset_id()来自 sway-lib-std/src/call_frames.sw,返回当前调用随附的资产类型;
  2. 仅当资产是AssetId::base()(基础资产,即燃料代币)时才记账,其他原生资产直接忽略(注释明确说明:"we're receiving other native assets and don't care about our balance of coins");
  3. msg_amount()来自 sway-lib-std/src/context.sw,其实现是读取当前调用上下文中的balance寄存器,返回本次调用发送过来的资产数量。

记账方式为"读取—累加—写回"三步:storage.balance.write(storage.balance.read() + msg_amount())

3.3 send_funds:权限校验、余额检查与转账

send_funds按严格的 Checks-Effects-Interactions 顺序执行:

  1. 身份检查(Check)msg_sender()来自 sway-lib-std/src/auth.sw,返回Result<Identity, AuthError>,封装了当前调用的发送者身份。实现侧先unwrap(),然后match解构Identity:只有Identity::Address(addr)addr == OWNER_ADDRESS时通过assert校验;其他任何身份(合约、predicate 等)直接revert(0)中止执行;
  2. 效果(Effect):读取当前storage.balanceassert(current_balance >= amount_to_send)防止超额提取,随后立即将扣减后的余额写回 storage。先改状态、后转账是防重入的关键;
  3. 交互(Interaction):最后调用std::asset::transfer完成实际转账。

3.4 std 库中 transfer 的底层实现

transfer定义在 sway-lib-std/src/asset.sw。从源码结构看,它是一个分发函数,按收款方身份选择不同路径:

pub fn transfer(to: Identity, asset_id: AssetId, amount: u64) { match to { Identity::Address(addr) => transfer_to_address(addr, asset_id, amount), Identity::ContractId(id) => force_transfer_to_contract(id, asset_id, amount), }; }
  • 转给合约时走force_transfer_to_contract,最终执行内联汇编的tr指令;
  • 转给地址时走transfer_to_address(见 asset.sw):由于tro指令需要占用一个"空的变量输出"槽位,实现会遍历交易输出(output_count/output_type/output_amount),找到amount为 0 的Output::Variable后执行tro指令;找不到空闲槽位则以revert(FAILED_TRANSFER_TO_ADDRESS_SIGNAL)失败。

文档中特别强调:"transfer()is not a call and thus not an interaction"——transfer只是向交易输出写资金,不会调用其他合约的代码,因此不构成可被重入的"交互"。即便如此,示例仍严格遵循 Checks-Effects-Interactions 顺序,把余额扣减放在transfer之前,形成纵深防御。

此外,asset.sw的文档注释明确列出了transfer的三种 revert 条件:金额超过合约该资产余额、金额为 0、转给地址时没有空闲变量输出。示例合约中的assert(current_balance >= amount_to_send)正是对第一种条件的业务侧提前拦截。

四、外部调用方视角:如何驱动这个钱包

仓库中配套的 examples/wallet_contract_caller_script/src/main.sw 演示了脚本如何持有同一份wallet_abi并发起跨项目调用:

script; use wallet_abi::Wallet; fn main() { let contract_address = 0x9299da6c73e6dc03eeabcce242bb347de3f5f56cd1c70926d76526d7ed199b8b; let caller = abi(Wallet, contract_address); let amount_to_send = 200; let recipient_address = Address::from(0x9299da6c73e6dc03eeabcce242bb347de3f5f56cd1c70926d76526d7ed199b8b); caller .send_funds { gas: 10000, coins: 0, asset_id: b256::zero(), }(amount_to_send, recipient_address); }

这段代码印证了"ABI 声明独立成项目"的价值:调用方不需要知道合约的内部实现,只需依赖wallet_abi,通过abi(Wallet, contract_address)得到带类型的客户端。调用参数块中的gascoinsasset_id分别是本次跨合约调用预留的 gas 上限、随调用发送的币数和资产 ID;send_fundspayable,这里coins传 0 也符合接口约定。

五、安全设计要点小结

机制实现位置作用
Owner 白名单main.sw 中match sender+revert(0)OWNER_ADDRESS可提取资金,非地址身份直接回滚
余额下限断言assert(current_balance >= amount_to_send)防止超额提取与无资产转账
先记账后转账扣减 storage 在transfer之前符合 Checks-Effects-Interactions,防重入
仅记基础资产receive_fundsmsg_asset_id() == AssetId::base()判断明确记账范围,避免混币歧义

综上,这个钱包示例以最小的代码量覆盖了 Sway 合约开发的核心链路:独立 ABI 库项目 →Forc.toml路径依赖 →contract实现 +storage声明 → std 库消息上下文与资产转账 → 脚本侧abi客户端调用,是理解 Sway 合约接口设计与安全编码模式的标准起点。

【免费下载链接】sway🌴 Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway

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

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

Karakeep 自托管部署用户管理与密码重置实战指南(FAQ 精讲)

Karakeep 自托管部署用户管理与密码重置实战指南&#xff08;FAQ 精讲&#xff09; 【免费下载链接】hoarder A self-hostable bookmark-everything app (links, notes and images) with AI-based automatic tagging and full text search 项目地址: https://gitcode.com/Git…

作者头像 李华
网站建设 2026/9/12 4:46:37

一致凸空间:定义、性质与偏微分方程应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 4:45:19

Prompt韧性工程:构建高鲁棒AI Agent的五大实战模块

我注意到您提供的项目标题是“GPT - 6 Astra 的使用焚诀”&#xff0c;但需要明确说明&#xff1a;截至目前&#xff08;2024年中&#xff09;&#xff0c;OpenAI 官方从未发布、命名或确认存在名为 “GPT-6” 或 “Astra” 的模型。所有网络上关于“GPT-6 Astra”的讨论、热搜…

作者头像 李华
网站建设 2026/9/12 4:43:10

迭代傅里叶变换算法IFTA:从原理到相位片工程实战

简介&#xff1a;IFTA&#xff08;迭代傅里叶变换&#xff09;算法的MATLAB实现资源&#xff0c;面向图像处理与信号处理方向的学生和研究人员&#xff0c;聚焦图像复原、去噪与频谱分析等典型应用。压缩包共含2个文件&#xff0c;一个可直接运行的MATLAB脚本与一幅经典标准测试…

作者头像 李华