Icon Link定义合约函数

最后,现在是时候编写我们的合约函数了。首先复制并粘贴我们之前概述的ABI。确保合约内的函数完全符合ABI是至关重要的;否则,编译器将生成错误。现在,将每个函数末尾的分号替换为花括号。还要修改abi SwayStoreimpl 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中的基本概念。

Icon Link1. 列出商品

我们的第一个函数使卖家能够列出待售商品。他们可以指定商品的价格,并提供一个字符串,引用了关于商品的离线存储数据。

#[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);
}

Icon Link更新列表存储

初始步骤涉及增加存储中的item_counter,它将作为商品的ID。在Sway中,所有存储变量都包含在storage关键字内,确保清晰度,并防止与其他变量名冲突。这也使开发人员可以轻松跟踪存储何时何地被访问或更改。Sway的标准库提供了read()write()try_read()方法来访问或操作合约存储。建议尽可能使用try_read(),以防止访问未初始化存储可能导致的问题。在本例中,我们读取已列出的商品当前数量,修改它,然后将更新后的数量重新存储到存储中,利用了这种良好组织且无冲突的存储系统。

当函数返回OptionResult类型时,我们可以使用unwrap()来访问其内部值。例如,try_read()返回一个Option类型。如果它产生Some,我们就获得其中包含的值;但如果它返回None,合约调用会立即停止。

// increment the item counter
storage.item_counter.write(storage.item_counter.try_read().unwrap() + 1);

Icon Link获取消息发送方

接下来,我们将检索列出商品的账户的Identity

要获取Identity,可以使用标准库中的msg_sender函数。msg_sender表示启动当前函数调用的实体(无论是用户地址还是其他合约地址)的地址。

此函数产生一个Result,它是一个枚举类型,可以是OK,也可以是错误。在预期可能产生错误的值时,使用Result类型。例如,在msg_sender的情况下,当涉及外部调用者且币的输入所有者不同时,无法确定调用方。在这种边缘情况下,会返回一个Err(AuthError)

enum Result<T, E> {
    Ok(T),
    Err(E),
}

在Sway中,可以使用letconst来定义变量。

// get the message sender
let sender = msg_sender().unwrap();

要检索内部值,可以使用unwrap方法。如果Result是OK,则返回其中包含的值;如果结果表示错误,则触发恐慌。

Icon Link创建新商品

您可以使用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,
};

Icon Link更新StorageMap

最后,使用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);

Icon Link2. 购买商品

接下来,我们的目标是允许买家购买已列出的商品。为实现此目的,我们需要:

  1. 接受买家作为函数参数的所需商品ID。
  2. 确保买家支付了正确的价格并使用了有效的币。
  3. 增加该商品的total_bought计数。
  4. 从商品的价格中扣除合约费用,并将剩余金额转移到卖家处。
#[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);
    }
}

Icon Link验证支付

我们可以使用标准库中的 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));

Icon Link更新购买存储

我们可以增加商品的 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);

Icon Link转移支付

最后,我们可以处理支付给卖家的款项。建议仅在所有存储修改完成后再转移资产,以防止重入攻击

对于达到特定价格阈值的商品,可以使用条件 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,以及要转移的币的数量。

Icon Link3. 获取商品

要获取商品的详细信息,我们可以创建一个只读函数,根据给定的商品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
}

Icon Link4. 初始化所有者

此方法仅在合约部署后的一次性设置合约所有者的 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

在这种情况下,需要注意 抢跑 Icon Link 的潜在风险,此代码尚未经过审计。

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;

Icon Link5. 提取资金

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);

Icon Link6. 获取总商品数

我们将介绍的最后一个函数是 get_count。这个简单的获取器函数返回存储在合约存储中的 item_counter 变量的值。

#[storage(read)]
fn get_count() -> u64 {
    return storage.item_counter.try_read().unwrap();
}

Icon Link回顾

您的 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();
    }
}