定义合约函数
最后,是时候编写我们的合约函数了。首先复制并粘贴我们之前概述的 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 中的基本概念。
1. 发布商品
我们的第一个功能使卖家能够列出待售商品。他们可以指定物料的价格,并提供引用有关物料的外部存储数据的字符串。
#[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 也可以是 ERROR。使用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正常,如果结果指示错误,则触发恐慌(rust 中的panic)。
创建新项目
您可以使用Item结构。使用item_countervalue from storage 作为 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,
};
更新 StorageMap
最后,将项目添加到item_map在存储中使用insert方法。对密钥使用相同的 ID,并将项目指定为其对应值。
// save the new item to storage using the counter value
storage.item_map.insert(storage.item_counter.try_read().unwrap(), new_item);
2. 购买商品
接下来,我们的目标是允许买家购买列出的商品。为此,我们需要:
接受买家提供的所需物料 ID 作为函数参数。 确保买家使用有效硬币支付正确的价格。 增加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语句接受两个参数:一个条件和一个在条件为 false 时记录的值。如果条件的计算结果为 false,则整个事务将回滚,不会留下任何更改。
在这种情况下,条件检查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结构.
// 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,则考虑到 Fuel 使用 9 十进制系统,这相当于 0.1 ETH。
如果金额超过 0.1 ETH,则确定佣金,然后从总额中扣除。
为了方便向物品所有者付款,transfer功能被利用。此函数源自标准库,需要三个参数:硬币发送到的身份、硬币的资产 ID 和用于转移的硬币数量。
3. 获取物品
为了获取项目的详细信息,我们可以创建一个只读函数,该函数返回Item给定项 ID 的结构。
#[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
}
4. 初始化所有者
此方法设置所有者的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;
5. 提取资金
这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);
6. 获取总项目数
我们将介绍的最后一个功能是get_count.这个简单的 getter 函数返回item_counter变量存储在合约的存储中。
#[storage(read)]
fn get_count() -> u64 {
return storage.item_counter.try_read().unwrap();
}
回顾
这SwayStore合同在您的main.sw现在应该看起来像这样,遵循我们之前写的所有其他内容:
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();
}
}