最后,现在是时候编写我们的合约函数了。首先复制并粘贴我们之前概述的ABI。确保合约内的函数完全符合ABI是至关重要的;否则,编译器将生成错误。现在,将每个函数末尾的分号替换为花括号。还要修改abi SwayStore
为impl SwayStore for Contract
,如下所示:
impl SwayStore for Contract {
#[storage(read, write)]
fn list_item(price: u64, metadata: str[20]){
}
#[storage(read, write), payable]
fn buy_item(item_id: u64) {
}
#[storage(read)]
fn get_item(item_id: u64) -> Item {
}
#[storage(read, write)]
fn initialize_owner() -> Identity {
}
#[storage(read)]
fn withdraw_funds(){
}
#[storage(read)]
fn get_count() -> u64{
}
}
此指南将首先展示上面列出的每个已完成函数。然后,我们将逐步解释每个部分,澄清特定的语法,并讨论Sway中的基本概念。
我们的第一个函数使卖家能够列出待售商品。他们可以指定商品的价格,并提供一个字符串,引用了关于商品的离线存储数据。
#[storage(read, write)]
fn list_item(price: u64, metadata: str[20]) {
// increment the item counter
storage.item_counter.write(storage.item_counter.try_read().unwrap() + 1);
// get the message sender
let sender = msg_sender().unwrap();
// configure the item
let new_item: Item = Item {
id: storage.item_counter.try_read().unwrap(),
price: price,
owner: sender,
metadata: metadata,
total_bought: 0,
};
// save the new item to storage using the counter value
storage.item_map.insert(storage.item_counter.try_read().unwrap(), new_item);
}
初始步骤涉及增加存储中的item_counter
,它将作为商品的ID。在Sway中,所有存储变量都包含在storage
关键字内,确保清晰度,并防止与其他变量名冲突。这也使开发人员可以轻松跟踪存储何时何地被访问或更改。Sway的标准库提供了read()
、write()
和try_read()
方法来访问或操作合约存储。建议尽可能使用try_read()
,以防止访问未初始化存储可能导致的问题。在本例中,我们读取已列出的商品当前数量,修改它,然后将更新后的数量重新存储到存储中,利用了这种良好组织且无冲突的存储系统。
当函数返回Option
或Result
类型时,我们可以使用unwrap()
来访问其内部值。例如,try_read()
返回一个Option
类型。如果它产生Some
,我们就获得其中包含的值;但如果它返回None
,合约调用会立即停止。
// increment the item counter
storage.item_counter.write(storage.item_counter.try_read().unwrap() + 1);
接下来,我们将检索列出商品的账户的Identity
。
要获取Identity
,可以使用标准库中的msg_sender
函数。msg_sender
表示启动当前函数调用的实体(无论是用户地址还是其他合约地址)的地址。
此函数产生一个Result
,它是一个枚举类型,可以是OK,也可以是错误。在预期可能产生错误的值时,使用Result
类型。例如,在msg_sender
的情况下,当涉及外部调用者且币的输入所有者不同时,无法确定调用方。在这种边缘情况下,会返回一个Err(AuthError)
。
enum Result<T, E> {
Ok(T),
Err(E),
}
在Sway中,可以使用let
或const
来定义变量。
// get the message sender
let sender = msg_sender().unwrap();
要检索内部值,可以使用unwrap
方法。如果Result
是OK,则返回其中包含的值;如果结果表示错误,则触发恐慌。
您可以使用Item
结构实例化一个新商品。使用存储中的item_counter
值作为ID,根据输入参数设置价格和元数据,并将total_bought
初始化为0。
由于owner
字段需要一个Identity
类型,因此应该使用从msg_sender()
获取的发送方值。
// configure the item
let new_item: Item = Item {
id: storage.item_counter.try_read().unwrap(),
price: price,
owner: sender,
metadata: metadata,
total_bought: 0,
};
最后,使用insert
方法将商品添加到存储中的item_map
。使用相同的ID作为键,并将商品指定为其对应的值。
// save the new item to storage using the counter value
storage.item_map.insert(storage.item_counter.try_read().unwrap(), new_item);
接下来,我们的目标是允许买家购买已列出的商品。为实现此目的,我们需要:
total_bought
计数。 #[storage(read, write), payable]
fn buy_item(item_id: u64) {
// get the asset id for the asset sent
let asset_id = msg_asset_id();
// require that the correct asset was sent
require(asset_id == AssetId::base(), InvalidError::IncorrectAssetId(asset_id));
// get the amount of coins sent
let amount = msg_amount();
// get the item to buy
let mut item = storage.item_map.get(item_id).try_read().unwrap();
// require that the amount is at least the price of the item
require(amount >= item.price, InvalidError::NotEnoughTokens(amount));
// update the total amount bought
item.total_bought += 1;
// update the item in the storage map
storage.item_map.insert(item_id, item);
// only charge commission if price is more than 0.1 ETH
if amount > 100_000_000 {
// keep a 5% commission
let commission = amount / 20;
let new_amount = amount - commission;
// send the payout minus commission to the seller
transfer(item.owner, asset_id, new_amount);
} else {
// send the full payout to the seller
transfer(item.owner, asset_id, amount);
}
}
我们可以使用标准库中的 msg_asset_id
函数获取交易中传输的币的资产ID。
let asset_id = msg_asset_id();
接下来,我们将利用 require
语句确保发送的资产是正确的。
require
语句接受两个参数:一个条件,以及在条件为假时记录的值。如果条件评估为假,则整个交易将被回滚,不留下任何更改。
在这种情况下,条件检查 asset_id
是否与基本资产ID 匹配 —— 基本区块链相关联的默认资产 - 使用 AssetId::base()
。例如,如果基本区块链是以太坊,则基本资产将是 ETH。
如果资产不匹配,例如,如果有人尝试使用不同的币购买商品,我们将触发之前定义的自定义错误,并传递 asset_id
。
require(asset_id == AssetId::base(), InvalidError::IncorrectAssetId(asset_id));
接下来,我们可以使用标准库中的 msg_amount
函数来获取交易中买家传输的币的数量。
let amount = msg_amount();
为了确保发送的金额不少于商品的价格,我们应该使用 item_id
参数获取商品的详细信息。
要获取存储映射中特定键的值,get
方法很方便,其中传递了键值。对于映射存储访问,使用了 try_read()
方法。由于该方法产生 Result
类型,因此可以应用 unwrap
方法来提取商品值。
let mut item = storage.item_map.get(item_id).try_read().unwrap();
在 Sway 中,默认情况下,所有变量都是不可变的,无论是用 let
还是 const
声明的。要修改任何变量的值,必须使用 mut
关键字声明为可变的。由于我们计划更新商品的 total_bought
值,因此应将其定义为可变的。
此外,确保发送给商品的币的数量不少于商品的价格至关重要。
require(amount >= item.price, InvalidError::NotEnoughTokens(amount));
我们可以增加商品的 total_bought
字段值,然后将其重新插入到 item_map
中。此操作将使用修订后的商品替换先前的值。
// update the total amount bought
item.total_bought += 1;
// update the item in the storage map
storage.item_map.insert(item_id, item);
最后,我们可以处理支付给卖家的款项。建议仅在所有存储修改完成后再转移资产,以防止重入攻击 。
对于达到特定价格阈值的商品,可以使用条件 if
语句扣除手续费。Sway 中的 if
语句结构与 JavaScript 中的类似,除了括号 ()
。
// only charge commission if price is more than 0.1 ETH
if amount > 100_000_000 {
// keep a 5% commission
let commission = amount / 20;
let new_amount = amount - commission;
// send the payout minus commission to the seller
transfer(item.owner, asset_id, new_amount);
} else {
// send the full payout to the seller
transfer(item.owner, asset_id, amount);
}
在上述 if 条件中,我们评估是否传输的金额超过了 100,000,000。对于诸如 100000000
这样的大数字,为了更清晰,我们可以将其表示为 100_000_000
。如果此合约的基础资产是 ETH,则相当于 0.1 ETH,因为 Fuel 使用 9 位小数系统。
如果金额超过 0.1 ETH,则确定佣金,然后从总金额中扣除。
为了支付给商品所有者,使用了 transfer
函数。该函数来自标准库,需要三个参数:将币发送到的 Identity、币的资产ID,以及要转移的币的数量。
要获取商品的详细信息,我们可以创建一个只读函数,根据给定的商品ID返回 Item
结构。
#[storage(read)]
fn get_item(item_id: u64) -> Item {
// returns the item for the given item_id
return storage.item_map.get(item_id).try_read().unwrap();
}
要在函数中返回一个值,可以使用 return
关键字,类似于 JavaScript。或者,可以省略最后一行的分号以返回该值,就像 Rust 中一样。
fn my_function_1(num: u64) -> u64{
// returns the num variable
return num;
}
fn my_function_2(num: u64) -> u64{
// returns the num variable
num
}
此方法仅在合约部署后的一次性设置合约所有者的 Identity
。
#[storage(read, write)]
fn initialize_owner() -> Identity {
let owner = storage.owner.try_read().unwrap();
// make sure the owner has NOT already been initialized
require(owner.is_none(), "owner already initialized");
// get the identity of the sender
let sender = msg_sender().unwrap();
// set the owner to the sender's identity
storage.owner.write(Option::Some(sender));
// return the owner
return sender;
}
为了确保此函数只能在合约部署后立即调用一次,非常重要的是所有者的值保持为 None
。我们可以使用 is_none
方法来实现此验证,该方法评估一个 Option 类型是否为 None
。
在这种情况下,需要注意 抢跑 的潜在风险,此代码尚未经过审计。
let owner = storage.owner.try_read().unwrap();
// make sure the owner has NOT already been initialized
require(owner.is_none(), "owner already initialized");
为了将 owner
分配为消息发送者,需要将 Result
类型转换为 Option
类型。
// get the identity of the sender
let sender = msg_sender().unwrap();
// set the owner to the sender's identity
storage.owner.write(Option::Some(sender));
最后,我们将返回消息发送者的 Identity
。
// return the owner
return sender;
withdraw_funds
函数允许所有者从合约中提取任何累积的资金。
#[storage(read)]
fn withdraw_funds() {
let owner = storage.owner.try_read().unwrap();
// make sure the owner has been initialized
require(owner.is_some(), "owner not initialized");
let sender = msg_sender().unwrap();
// require the sender to be the owner
require(sender == owner.unwrap(), InvalidError::OnlyOwner(sender));
// get the current balance of this contract for the base asset
let amount = this_balance(AssetId::base());
// require the contract balance to be more than 0
require(amount > 0, InvalidError::NotEnoughTokens(amount));
// send the amount to the owner
transfer(owner.unwrap(), AssetId::base(), amount);
}
首先,我们将确保所有者已初始化为特定地址。
let owner = storage.owner.try_read().unwrap();
// make sure the owner has been initialized
require(owner.is_some(), "owner not initialized");
接下来,我们将验证尝试提取资金的个体是否确实是所有者。
let sender = msg_sender().unwrap();
// require the sender to be the owner
require(sender == owner.unwrap(), InvalidError::OnlyOwner(sender));
此外,我们可以使用标准库中的 this_balance
函数来确认可供提取的资金的可用性。该函数返回合约的当前余额。
// get the current balance of this contract for the base asset
let amount = this_balance(AssetId::base());
// require the contract balance to be more than 0
require(amount > 0, InvalidError::NotEnoughTokens(amount));
最后,我们将合约的全部余额转移到所有者。
// send the amount to the owner
transfer(owner.unwrap(), AssetId::base(), amount);
我们将介绍的最后一个函数是 get_count
。这个简单的获取器函数返回存储在合约存储中的 item_counter
变量的值。
#[storage(read)]
fn get_count() -> u64 {
return storage.item_counter.try_read().unwrap();
}
您的 main.sw
中的 SwayStore
合约实现现在应该如下所示,跟随我们之前编写的所有内容:
contract;
use std::{
auth::msg_sender,
call_frames::msg_asset_id,
context::{
msg_amount,
this_balance,
},
asset::transfer,
hash::Hash,
};
struct Item {
id: u64,
price: u64,
owner: Identity,
metadata: str[20],
total_bought: u64,
}
abi SwayStore {
// a function to list an item for sale
// takes the price and metadata as args
#[storage(read, write)]
fn list_item(price: u64, metadata: str[20]);
// a function to buy an item
// takes the item id as the arg
#[storage(read, write), payable]
fn buy_item(item_id: u64);
// a function to get a certain item
#[storage(read)]
fn get_item(item_id: u64) -> Item;
// a function to set the contract owner
#[storage(read, write)]
fn initialize_owner() -> Identity;
// a function to withdraw contract funds
#[storage(read)]
fn withdraw_funds();
// return the number of items listed
#[storage(read)]
fn get_count() -> u64;
}
storage {
// counter for total items listed
item_counter: u64 = 0,
// map of item IDs to Items
item_map: StorageMap<u64, Item> = StorageMap {},
// owner of the contract
owner: Option<Identity> = Option::None,
}
enum InvalidError {
IncorrectAssetId: AssetId,
NotEnoughTokens: u64,
OnlyOwner: Identity,
}
impl SwayStore for Contract {
#[storage(read, write)]
fn list_item(price: u64, metadata: str[20]) {
// increment the item counter
storage.item_counter.write(storage.item_counter.try_read().unwrap() + 1);
// get the message sender
let sender = msg_sender().unwrap();
// configure the item
let new_item: Item = Item {
id: storage.item_counter.try_read().unwrap(),
price: price,
owner: sender,
metadata: metadata,
total_bought: 0,
};
// save the new item to storage using the counter value
storage.item_map.insert(storage.item_counter.try_read().unwrap(), new_item);
}
#[storage(read, write), payable]
fn buy_item(item_id: u64) {
// get the asset id for the asset sent
let asset_id = msg_asset_id();
// require that the correct asset was sent
require(asset_id == AssetId::base(), InvalidError::IncorrectAssetId(asset_id));
// get the amount of coins sent
let amount = msg_amount();
// get the item to buy
let mut item = storage.item_map.get(item_id).try_read().unwrap();
// require that the amount is at least the price of the item
require(amount >= item.price, InvalidError::NotEnoughTokens(amount));
// update the total amount bought
item.total_bought += 1;
// update the item in the storage map
storage.item_map.insert(item_id, item);
// only charge commission if price is more than 0.1 ETH
if amount > 100_000_000 {
// keep a 5% commission
let commission = amount / 20;
let new_amount = amount - commission;
// send the payout minus commission to the seller
transfer(item.owner, asset_id, new_amount);
} else {
// send the full payout to the seller
transfer(item.owner, asset_id, amount);
}
}
#[storage(read)]
fn get_item(item_id: u64) -> Item {
// returns the item for the given item_id
return storage.item_map.get(item_id).try_read().unwrap();
}
#[storage(read, write)]
fn initialize_owner() -> Identity {
let owner = storage.owner.try_read().unwrap();
// make sure the owner has NOT already been initialized
require(owner.is_none(), "owner already initialized");
// get the identity of the sender
let sender = msg_sender().unwrap();
// set the owner to the sender's identity
storage.owner.write(Option::Some(sender));
// return the owner
return sender;
}
#[storage(read)]
fn withdraw_funds() {
let owner = storage.owner.try_read().unwrap();
// make sure the owner has been initialized
require(owner.is_some(), "owner not initialized");
let sender = msg_sender().unwrap();
// require the sender to be the owner
require(sender == owner.unwrap(), InvalidError::OnlyOwner(sender));
// get the current balance of this contract for the base asset
let amount = this_balance(AssetId::base());
// require the contract balance to be more than 0
require(amount > 0, InvalidError::NotEnoughTokens(amount));
// send the amount to the owner
transfer(owner.unwrap(), AssetId::base(), amount);
}
#[storage(read)]
fn get_count() -> u64 {
return storage.item_counter.try_read().unwrap();
}
}